{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"marketing-changelogs-grouped-changelog","type":"registry:block","title":"Grouped Changelog","files":[{"path":"components/blocks/marketing/changelogs/grouped-changelog.tsx","content":"import * as React from 'react';\nimport { Badge } from '@/components/ui/badge';\nimport { Card, CardContent } from '@/components/ui/card';\nimport {\n  Accordion,\n  AccordionContent,\n  AccordionItem,\n  AccordionTrigger,\n} from '@/components/ui/accordion';\nimport { cn } from '@/lib/utils';\n\ninterface ChangelogItem {\n  version: string;\n  date: string;\n  isLatest: boolean;\n  changes: {\n    type: 'feature' | 'improvement' | 'bugfix' | 'breaking';\n    description: string;\n  }[];\n}\n\ninterface TimeGroup {\n  year: number;\n  quarters: {\n    quarter: number;\n    releases: ChangelogItem[];\n  }[];\n}\n\nexport default function GroupedChangelog() {\n  // Sample data\n  const changelogData: ChangelogItem[] = [\n    {\n      version: '2.0.0',\n      date: '2023-04-15',\n      isLatest: true,\n      changes: [\n        {\n          type: 'feature',\n          description: 'Completely redesigned dashboard interface',\n        },\n        {\n          type: 'feature',\n          description: 'Added dark mode support across all pages',\n        },\n        { type: 'improvement', description: 'Improved performance by 40%' },\n        { type: 'bugfix', description: 'Fixed multiple accessibility issues' },\n        {\n          type: 'breaking',\n          description: 'API endpoints restructured for v2',\n        },\n      ],\n    },\n    {\n      version: '1.9.0',\n      date: '2023-02-10',\n      isLatest: false,\n      changes: [\n        { type: 'feature', description: 'Added export to PDF functionality' },\n        { type: 'improvement', description: 'Enhanced search performance' },\n        { type: 'bugfix', description: 'Fixed login issues on Safari' },\n      ],\n    },\n    {\n      version: '1.8.5',\n      date: '2022-12-07',\n      isLatest: false,\n      changes: [\n        {\n          type: 'improvement',\n          description: 'Year-end performance optimizations',\n        },\n        { type: 'bugfix', description: 'Fixed data visualization issues' },\n      ],\n    },\n    {\n      version: '1.8.0',\n      date: '2022-10-20',\n      isLatest: false,\n      changes: [\n        { type: 'feature', description: 'New data visualization options' },\n        { type: 'feature', description: 'Added team collaboration features' },\n        { type: 'improvement', description: 'Updated documentation' },\n      ],\n    },\n    {\n      version: '1.7.0',\n      date: '2022-08-05',\n      isLatest: false,\n      changes: [\n        { type: 'feature', description: 'Added user roles and permissions' },\n        { type: 'bugfix', description: 'Fixed several UI glitches' },\n      ],\n    },\n    {\n      version: '1.6.0',\n      date: '2022-05-12',\n      isLatest: false,\n      changes: [\n        { type: 'feature', description: 'Integrated third-party API services' },\n        { type: 'improvement', description: 'Enhanced mobile experience' },\n      ],\n    },\n    {\n      version: '1.5.0',\n      date: '2022-02-28',\n      isLatest: false,\n      changes: [\n        { type: 'feature', description: 'Added custom report builder' },\n        { type: 'improvement', description: 'Improved data loading times' },\n      ],\n    },\n    {\n      version: '1.4.0',\n      date: '2021-11-18',\n      isLatest: false,\n      changes: [\n        { type: 'feature', description: 'Added multi-language support' },\n        { type: 'bugfix', description: 'Fixed account settings issues' },\n      ],\n    },\n  ];\n\n  // Group data by year and quarter\n  const groupedData: TimeGroup[] = React.useMemo(() => {\n    const groups: Record<number, TimeGroup> = {};\n\n    changelogData.forEach((item) => {\n      const date = new Date(item.date);\n      const year = date.getFullYear();\n      const quarter = Math.floor(date.getMonth() / 3) + 1;\n\n      if (!groups[year]) {\n        groups[year] = { year, quarters: [] };\n      }\n\n      let quarterGroup = groups[year].quarters.find(\n        (q) => q.quarter === quarter\n      );\n      if (!quarterGroup) {\n        quarterGroup = { quarter, releases: [] };\n        groups[year].quarters.push(quarterGroup);\n      }\n\n      quarterGroup.releases.push(item);\n    });\n\n    // Sort quarters in descending order\n    Object.values(groups).forEach((yearGroup) => {\n      yearGroup.quarters.sort((a, b) => b.quarter - a.quarter);\n      yearGroup.quarters.forEach((quarter) => {\n        quarter.releases.sort(\n          (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()\n        );\n      });\n    });\n\n    // Sort years in descending order\n    return Object.values(groups).sort((a, b) => b.year - a.year);\n  }, [changelogData]);\n\n  // Get quarterly name\n  const getQuarterName = (quarter: number) => {\n    return `Q${quarter}`;\n  };\n\n  // Format date for display\n  const formatDate = (dateString: string) => {\n    const options: Intl.DateTimeFormatOptions = {\n      year: 'numeric',\n      month: 'long',\n      day: 'numeric',\n    };\n    return new Date(dateString).toLocaleDateString('en-US', options);\n  };\n\n  // Change type styling\n  const getChangeTypeStyles = (type: string) => {\n    switch (type) {\n      case 'feature':\n        return 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400 border-transparent';\n      case 'improvement':\n        return 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400 border-transparent';\n      case 'bugfix':\n        return 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400 border-transparent';\n      case 'breaking':\n        return 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-400 border-transparent';\n      default:\n        return 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200 border-transparent';\n    }\n  };\n\n  // Get change type label\n  const getChangeTypeLabel = (type: string) => {\n    switch (type) {\n      case 'feature':\n        return 'New';\n      case 'improvement':\n        return 'Improved';\n      case 'bugfix':\n        return 'Fixed';\n      case 'breaking':\n        return 'Breaking';\n      default:\n        return type;\n    }\n  };\n\n  return (\n    <div className=\"container mx-auto max-w-4xl px-4 py-8\">\n      <div className=\"mb-8\">\n        <h2 className=\"text-3xl font-bold tracking-tight\">Changelog</h2>\n        <p className=\"text-muted-foreground mt-2\">\n          History of changes organized by year and quarter\n        </p>\n      </div>\n\n      <div className=\"space-y-6\">\n        {groupedData.map((yearGroup) => (\n          <Accordion\n            type=\"single\"\n            collapsible\n            className=\"w-full\"\n            defaultValue={yearGroup.year.toString()}\n            key={yearGroup.year}\n          >\n            <AccordionItem\n              value={yearGroup.year.toString()}\n              className=\"border-b-0\"\n            >\n              <AccordionTrigger className=\"bg-muted/50 hover:bg-muted rounded-lg px-4 py-3 hover:no-underline data-[state=open]:rounded-b-none\">\n                <span className=\"text-xl font-bold\">{yearGroup.year}</span>\n              </AccordionTrigger>\n              <AccordionContent className=\"pt-0 pb-0\">\n                <div className=\"space-y-6 pb-6\">\n                  {yearGroup.quarters.map((quarter) => (\n                    <div\n                      key={`${yearGroup.year}-Q${quarter.quarter}`}\n                      className=\"rounded-lg border border-dashed p-3 sm:p-4\"\n                    >\n                      <h3 className=\"mb-4 flex flex-wrap items-center gap-2 text-lg font-semibold\">\n                        <span className=\"text-muted-foreground text-sm font-medium\">\n                          {yearGroup.year}\n                        </span>\n                        <Badge variant=\"outline\" className=\"bg-muted/50\">\n                          {getQuarterName(quarter.quarter)}\n                        </Badge>\n                      </h3>\n                      <div className=\"space-y-4\">\n                        {quarter.releases.map((release) => (\n                          <Card\n                            key={release.version}\n                            className=\"overflow-hidden pt-0\"\n                          >\n                            <CardContent className=\"p-0\">\n                              <div className=\"bg-muted/30 border-b p-3 sm:p-4\">\n                                <div className=\"flex flex-col gap-2 sm:flex-row sm:items-center\">\n                                  <div className=\"flex flex-wrap items-center gap-2\">\n                                    <h4 className=\"text-lg font-bold\">\n                                      v{release.version}\n                                    </h4>\n                                    {release.isLatest && (\n                                      <Badge className=\"bg-primary text-primary-foreground\">\n                                        Latest\n                                      </Badge>\n                                    )}\n                                  </div>\n                                  <time className=\"text-muted-foreground text-sm sm:ml-auto\">\n                                    {formatDate(release.date)}\n                                  </time>\n                                </div>\n                              </div>\n                              <ul className=\"divide-y\">\n                                {release.changes.map((change, i) => (\n                                  <li\n                                    key={`${release.version}-${i}`}\n                                    className=\"flex flex-col gap-2 p-3 sm:flex-row sm:items-start sm:p-4\"\n                                  >\n                                    <Badge\n                                      variant=\"outline\"\n                                      className={cn(\n                                        'min-w-[60px] justify-center self-start rounded px-1.5 text-xs capitalize',\n                                        getChangeTypeStyles(change.type)\n                                      )}\n                                    >\n                                      {getChangeTypeLabel(change.type)}\n                                    </Badge>\n                                    <span className=\"break-words sm:ml-2\">\n                                      {change.description}\n                                    </span>\n                                  </li>\n                                ))}\n                              </ul>\n                            </CardContent>\n                          </Card>\n                        ))}\n                      </div>\n                    </div>\n                  ))}\n                </div>\n              </AccordionContent>\n            </AccordionItem>\n          </Accordion>\n        ))}\n      </div>\n    </div>\n  );\n}\n","type":"registry:component","target":"components/blocks/marketing/changelogs/grouped-changelog.tsx"},{"path":"components/ui/badge.tsx","content":"import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { Slot } from \"radix-ui\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst badgeVariants = cva(\n  \"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-primary text-primary-foreground [a]:hover:bg-primary/80\",\n        secondary:\n          \"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80\",\n        destructive:\n          \"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20\",\n        outline:\n          \"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground\",\n        ghost:\n          \"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  }\n)\n\nfunction Badge({\n  className,\n  variant = \"default\",\n  asChild = false,\n  ...props\n}: React.ComponentProps<\"span\"> &\n  VariantProps<typeof badgeVariants> & { asChild?: boolean }) {\n  const Comp = asChild ? Slot.Root : \"span\"\n\n  return (\n    <Comp\n      data-slot=\"badge\"\n      data-variant={variant}\n      className={cn(badgeVariants({ variant }), className)}\n      {...props}\n    />\n  )\n}\n\nexport { Badge, badgeVariants }\n","type":"registry:ui","target":"components/ui/badge.tsx"},{"path":"components/ui/card.tsx","content":"import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Card({\n  className,\n  size = \"default\",\n  ...props\n}: React.ComponentProps<\"div\"> & { size?: \"default\" | \"sm\" }) {\n  return (\n    <div\n      data-slot=\"card\"\n      data-size={size}\n      className={cn(\n        \"group/card flex flex-col gap-6 overflow-hidden rounded-xl bg-card py-6 text-sm text-card-foreground shadow-xs ring-1 ring-foreground/10 has-[>img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction CardHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-header\"\n      className={cn(\n        \"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-6 group-data-[size=sm]/card:px-4 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-6 group-data-[size=sm]/card:[.border-b]:pb-4\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction CardTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-title\"\n      className={cn(\n        \"font-heading text-base leading-normal font-medium group-data-[size=sm]/card:text-sm\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction CardDescription({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-description\"\n      className={cn(\"text-sm text-muted-foreground\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction CardAction({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-action\"\n      className={cn(\n        \"col-start-2 row-span-2 row-start-1 self-start justify-self-end\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction CardContent({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-content\"\n      className={cn(\"px-6 group-data-[size=sm]/card:px-4\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction CardFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-footer\"\n      className={cn(\n        \"flex items-center rounded-b-xl px-6 group-data-[size=sm]/card:px-4 [.border-t]:pt-6 group-data-[size=sm]/card:[.border-t]:pt-4\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nexport {\n  Card,\n  CardHeader,\n  CardFooter,\n  CardTitle,\n  CardAction,\n  CardDescription,\n  CardContent,\n}\n","type":"registry:ui","target":"components/ui/card.tsx"},{"path":"components/ui/accordion.tsx","content":"\"use client\"\n\nimport * as React from \"react\"\nimport { Accordion as AccordionPrimitive } from \"radix-ui\"\n\nimport { cn } from \"@/lib/utils\"\nimport { ChevronDownIcon, ChevronUpIcon } from \"lucide-react\"\n\nfunction Accordion({\n  className,\n  ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Root>) {\n  return (\n    <AccordionPrimitive.Root\n      data-slot=\"accordion\"\n      className={cn(\"flex w-full flex-col\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction AccordionItem({\n  className,\n  ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Item>) {\n  return (\n    <AccordionPrimitive.Item\n      data-slot=\"accordion-item\"\n      className={cn(\"not-last:border-b\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction AccordionTrigger({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {\n  return (\n    <AccordionPrimitive.Header className=\"flex\">\n      <AccordionPrimitive.Trigger\n        data-slot=\"accordion-trigger\"\n        className={cn(\n          \"group/accordion-trigger relative flex flex-1 items-start justify-between rounded-md border border-transparent py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:after:border-ring disabled:pointer-events-none disabled:opacity-50 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4 **:data-[slot=accordion-trigger-icon]:text-muted-foreground\",\n          className\n        )}\n        {...props}\n      >\n        {children}\n        <ChevronDownIcon data-slot=\"accordion-trigger-icon\" className=\"pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden\" />\n        <ChevronUpIcon data-slot=\"accordion-trigger-icon\" className=\"pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline\" />\n      </AccordionPrimitive.Trigger>\n    </AccordionPrimitive.Header>\n  )\n}\n\nfunction AccordionContent({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Content>) {\n  return (\n    <AccordionPrimitive.Content\n      data-slot=\"accordion-content\"\n      className=\"overflow-hidden text-sm data-open:animate-accordion-down data-closed:animate-accordion-up\"\n      {...props}\n    >\n      <div\n        className={cn(\n          \"pt-0 pb-4 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4\",\n          className\n        )}\n      >\n        {children}\n      </div>\n    </AccordionPrimitive.Content>\n  )\n}\n\nexport { Accordion, AccordionItem, AccordionTrigger, AccordionContent }\n","type":"registry:ui","target":"components/ui/accordion.tsx"}],"description":"Updates clustered by month or release so related entries sit together. Good for products with frequent shipping where context across versions matters.","dependencies":["class-variance-authority","lucide-react","radix-ui"]}