{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "changelog",
  "title": "Changelog",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-vue/blocks/changelog/Changelog.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport type { HTMLAttributes } from 'vue'\nimport { Bell } from 'lucide-vue-next'\nimport { cn } from '@/lib/utils'\nimport { Badge, type BadgeVariants } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { SectionCard } from '@/components/ui/section-card'\n\ntype EntryType = 'new' | 'improved' | 'fixed' | 'breaking'\n\ninterface ChangelogEntry {\n  type: EntryType\n  title: string\n  description?: string\n}\n\ninterface Release {\n  version: string\n  /** ISO date string, e.g. '2026-08-14'. */\n  date: string\n  /** Overrides the default \"first release in the array is the latest\" rule. */\n  latest?: boolean\n  entries: ChangelogEntry[]\n}\n\nconst props = withDefaults(\n  defineProps<{\n    releases?: Release[]\n    title?: string\n    description?: string\n    subscribe?: boolean\n    subscribeVariant?: 'default' | 'outline'\n    scrollable?: boolean\n    class?: HTMLAttributes['class']\n  }>(),\n  {\n    title: 'Changelog',\n    description: 'New features, improvements, and fixes — newest first.',\n    subscribe: true,\n    subscribeVariant: 'outline',\n    scrollable: false,\n  },\n)\n\nconst entryTone: Record<EntryType, BadgeVariants['variant']> = {\n  new: 'success',\n  improved: 'info',\n  fixed: 'warning',\n  breaking: 'destructive',\n}\n\nconst entryLabel: Record<EntryType, string> = {\n  new: 'New',\n  improved: 'Improved',\n  fixed: 'Fixed',\n  breaking: 'Breaking',\n}\n\nconst stubReleases: Release[] = [\n  {\n    version: 'v2.4.0',\n    date: '2026-08-14',\n    entries: [\n      {\n        type: 'new',\n        title: 'Command palette',\n        description: 'Global ⌘K palette with fuzzy search and full keyboard navigation.',\n      },\n      { type: 'improved', title: 'Table virtualization', description: 'Tables stay at 60fps past 10k rows.' },\n      { type: 'fixed', title: 'Dark mode contrast', description: 'Sidebar badges now meet AA contrast in dark mode.' },\n    ],\n  },\n  {\n    version: 'v2.3.0',\n    date: '2026-07-30',\n    entries: [\n      {\n        type: 'new',\n        title: 'Saved views',\n        description: 'Pin filtered table views to the sidebar and share them with your team.',\n      },\n      {\n        type: 'improved',\n        title: 'CSV import',\n        description: 'Merged cells and formula columns are flattened on import.',\n      },\n      { type: 'fixed', title: 'Scheduled exports', description: 'Exports no longer drift across DST boundaries.' },\n    ],\n  },\n  {\n    version: 'v2.2.1',\n    date: '2026-07-08',\n    entries: [\n      {\n        type: 'breaking',\n        title: 'Node 20+ required',\n        description: 'Node 18 reached end of life; the CLI now targets Node 20 and newer.',\n      },\n      {\n        type: 'fixed',\n        title: 'Safari login loop',\n        description: 'Session cookies set on a redirect were dropped by ITP.',\n      },\n      {\n        type: 'fixed',\n        title: 'Duplicate webhooks',\n        description: 'Retries after a timeout no longer double-deliver events.',\n      },\n    ],\n  },\n]\n\n// Named `allReleases` because defineProps exposes `releases` directly to the template.\nconst allReleases = computed(() => props.releases ?? stubReleases)\n\nfunction isLatest(release: Release, index: number) {\n  return release.latest ?? index === 0\n}\n\nconst dateFormatter = new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric' })\n\nfunction formatDate(date: string) {\n  // Pin the time so a UTC-stored ISO date cannot shift a day in negative offsets.\n  return dateFormatter.format(new Date(`${date}T00:00:00`))\n}\n</script>\n\n<template>\n  <SectionCard data-slot=\"changelog\" :title=\"title\" :description=\"description\" :class=\"$props.class\">\n    <template v-if=\"subscribe\" #header-action>\n      <Button :variant=\"subscribeVariant\" size=\"sm\">\n        <Bell aria-hidden=\"true\" />\n        Subscribe\n      </Button>\n    </template>\n\n    <p v-if=\"allReleases.length === 0\" class=\"text-muted-foreground py-6 text-center text-sm\">No releases yet.</p>\n\n    <!-- Version groups. Each entry li carries its own border-l segment so the\n         rail stays continuous through row spacing but breaks at version headers. -->\n    <ol v-else :class=\"cn('space-y-8', scrollable && 'max-h-96 overflow-y-auto pr-1')\">\n      <li v-for=\"(release, ri) in allReleases\" :key=\"release.version\">\n        <div class=\"flex items-center gap-2.5\">\n          <h3 class=\"font-mono text-sm font-semibold tracking-tight break-all\">{{ release.version }}</h3>\n          <Badge v-if=\"isLatest(release, ri)\">Latest</Badge>\n          <time :datetime=\"release.date\" class=\"text-muted-foreground ml-auto text-xs\">\n            {{ formatDate(release.date) }}\n          </time>\n        </div>\n\n        <ol class=\"mt-3\">\n          <li\n            v-for=\"(entry, ei) in release.entries\"\n            :key=\"`${release.version}-${ei}`\"\n            class=\"border-border border-l pb-5 pl-6 last:pb-0\"\n          >\n            <div class=\"flex items-start gap-3\">\n              <!-- In-flow dot pulled back over the rail; ring punches out the line\n                   behind it (same trick as TimelineMedia's connector). -->\n              <span\n                aria-hidden=\"true\"\n                class=\"bg-muted-foreground/40 ring-background mt-1.5 -ml-[30px] size-2.5 shrink-0 rounded-full ring-4\"\n              />\n              <div class=\"min-w-0\">\n                <div class=\"flex flex-wrap items-center gap-x-2 gap-y-1\">\n                  <Badge :variant=\"entryTone[entry.type]\">{{ entryLabel[entry.type] }}</Badge>\n                  <p class=\"text-sm font-medium\">{{ entry.title }}</p>\n                </div>\n                <p v-if=\"entry.description\" class=\"text-muted-foreground mt-1 text-sm leading-relaxed\">\n                  {{ entry.description }}\n                </p>\n              </div>\n            </div>\n          </li>\n        </ol>\n      </li>\n    </ol>\n  </SectionCard>\n</template>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/Changelog.vue"
    }
  ],
  "dependencies": [
    "lucide-vue-next"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/vue/badge.json",
    "https://uipkge.dev/r/vue/button.json",
    "https://uipkge.dev/r/vue/section-card.json"
  ],
  "description": "Release-notes timeline in a SectionCard: Subscribe header action, version groups with a monospace tag, Latest badge and right-aligned date, and entries pairing a typed badge (New / Improved / Fixed / Breaking) with a title and optional description along a continuous border-l rail with dot markers. Pass `releases` to replace the stub; first entry is treated as latest unless a release sets `latest`.",
  "categories": [
    "marketing"
  ]
}