{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "activity-log",
  "type": "registry:component",
  "title": "Activity Log",
  "description": "Paginated activity feed for audit history and analytics changes.",
  "dependencies": [
    "@vllnt/ui@^0.2.1"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/activity-log/activity-log.tsx",
      "content": "\"use client\";\n\nimport { forwardRef, useMemo, useState } from \"react\";\n\nimport { ArrowRight, ChevronLeft, ChevronRight } from \"lucide-react\";\n\nimport { cn } from \"@vllnt/ui\";\nimport { Avatar, AvatarFallback } from \"@vllnt/ui\";\nimport { Badge } from \"@vllnt/ui\";\nimport { Button } from \"@vllnt/ui\";\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@vllnt/ui\";\nimport { ScrollArea } from \"@vllnt/ui\";\nimport { Separator } from \"@vllnt/ui\";\n\nexport type ActivityLogTone = \"danger\" | \"default\" | \"success\" | \"warning\";\n\nexport type ActivityLogItem = {\n  action: string;\n  actor: string;\n  description?: string;\n  id: string;\n  scope?: string;\n  target?: string;\n  timestamp: string;\n  tone?: ActivityLogTone;\n};\n\nexport type ActivityLogProps = React.ComponentPropsWithoutRef<typeof Card> & {\n  defaultPage?: number;\n  description?: string;\n  emptyMessage?: string;\n  items: ActivityLogItem[];\n  onPageChange?: (page: number) => void;\n  page?: number;\n  pageSize?: number;\n  title?: string;\n};\n\ntype ActivityToneConfig = {\n  badgeClassName: string;\n  markerClassName: string;\n};\n\ntype ActivityRowProps = {\n  item: ActivityLogItem;\n};\n\ntype PaginationControlsProps = {\n  currentPage: number;\n  onPageChange: (page: number) => void;\n  pageNumbers: number[];\n  totalPages: number;\n};\n\nconst toneConfig: Record<ActivityLogTone, ActivityToneConfig> = {\n  danger: {\n    badgeClassName:\n      \"border-destructive/20 bg-destructive/10 text-destructive dark:text-destructive\",\n    markerClassName: \"bg-destructive\",\n  },\n  default: {\n    badgeClassName: \"border-border bg-muted text-muted-foreground\",\n    markerClassName: \"bg-primary\",\n  },\n  success: {\n    badgeClassName:\n      \"border-emerald-500/20 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300\",\n    markerClassName: \"bg-emerald-500\",\n  },\n  warning: {\n    badgeClassName:\n      \"border-amber-500/20 bg-amber-500/10 text-amber-700 dark:text-amber-300\",\n    markerClassName: \"bg-amber-500\",\n  },\n};\n\nfunction getInitials(name: string): string {\n  return name\n    .split(\" \")\n    .map((segment) => segment[0])\n    .join(\"\")\n    .slice(0, 2)\n    .toUpperCase();\n}\n\nfunction buildPageNumbers(currentPage: number, totalPages: number): number[] {\n  if (totalPages <= 1) return [1];\n\n  const start = Math.max(1, currentPage - 1);\n  const end = Math.min(totalPages, start + 2);\n  const normalizedStart = Math.max(1, end - 2);\n\n  return Array.from(\n    { length: end - normalizedStart + 1 },\n    (_, index) => normalizedStart + index,\n  );\n}\n\nfunction ActivityLogHeader({\n  currentPage,\n  description,\n  title,\n  totalPages,\n}: {\n  currentPage: number;\n  description?: string;\n  title: string;\n  totalPages: number;\n}) {\n  return (\n    <CardHeader>\n      <div className=\"flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between\">\n        <div>\n          <CardTitle>{title}</CardTitle>\n          {description ? (\n            <CardDescription>{description}</CardDescription>\n          ) : null}\n        </div>\n        <Badge className=\"w-fit\" variant=\"outline\">\n          Page {currentPage} of {totalPages}\n        </Badge>\n      </div>\n    </CardHeader>\n  );\n}\n\nfunction PaginationControls({\n  currentPage,\n  onPageChange,\n  pageNumbers,\n  totalPages,\n}: PaginationControlsProps) {\n  return (\n    <div className=\"flex flex-wrap items-center gap-2\">\n      <Button\n        disabled={currentPage === 1}\n        onClick={() => {\n          onPageChange(currentPage - 1);\n        }}\n        size=\"sm\"\n        variant=\"outline\"\n      >\n        <ChevronLeft className=\"size-4\" />\n        Previous\n      </Button>\n      {pageNumbers.map((pageNumber) => (\n        <Button\n          aria-label={`Go to page ${pageNumber}`}\n          key={pageNumber}\n          onClick={() => {\n            onPageChange(pageNumber);\n          }}\n          size=\"sm\"\n          variant={pageNumber === currentPage ? \"default\" : \"outline\"}\n        >\n          {pageNumber}\n        </Button>\n      ))}\n      <Button\n        disabled={currentPage === totalPages}\n        onClick={() => {\n          onPageChange(currentPage + 1);\n        }}\n        size=\"sm\"\n        variant=\"outline\"\n      >\n        Next\n        <ChevronRight className=\"size-4\" />\n      </Button>\n    </div>\n  );\n}\n\nfunction ActivityRow({ item }: ActivityRowProps) {\n  const palette = toneConfig[item.tone ?? \"default\"];\n\n  return (\n    <li className=\"relative pl-12\">\n      <span\n        aria-hidden=\"true\"\n        className=\"absolute bottom-[-1.5rem] left-[18px] top-11 w-px bg-border last:hidden\"\n      />\n      <span\n        aria-hidden=\"true\"\n        className={cn(\n          \"absolute left-4 top-3 size-3 rounded-full ring-4 ring-background\",\n          palette.markerClassName,\n        )}\n      />\n      <div className=\"rounded-lg border bg-background/70 p-4\">\n        <div className=\"flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between\">\n          <div className=\"flex min-w-0 items-start gap-3\">\n            <Avatar className=\"size-9 border bg-muted\">\n              <AvatarFallback>{getInitials(item.actor)}</AvatarFallback>\n            </Avatar>\n            <div className=\"min-w-0 space-y-1\">\n              <div className=\"flex flex-wrap items-center gap-2\">\n                <span className=\"font-medium text-foreground\">\n                  {item.actor}\n                </span>\n                <ArrowRight className=\"size-3.5 text-muted-foreground\" />\n                <span className=\"text-sm text-muted-foreground\">\n                  {item.action}\n                </span>\n                {item.target ? (\n                  <span className=\"truncate text-sm font-medium text-foreground\">\n                    {item.target}\n                  </span>\n                ) : null}\n              </div>\n              {item.description ? (\n                <p className=\"text-sm text-muted-foreground\">\n                  {item.description}\n                </p>\n              ) : null}\n            </div>\n          </div>\n          <div className=\"flex shrink-0 flex-wrap items-center gap-2 sm:justify-end\">\n            {item.scope ? (\n              <Badge className={palette.badgeClassName} variant=\"outline\">\n                {item.scope}\n              </Badge>\n            ) : null}\n            <span className=\"text-xs text-muted-foreground\">\n              {item.timestamp}\n            </span>\n          </div>\n        </div>\n      </div>\n    </li>\n  );\n}\n\nfunction ActivityLogBody({\n  currentPage,\n  emptyMessage,\n  items,\n  onPageChange,\n  pageNumbers,\n  pageSize,\n  totalPages,\n}: {\n  currentPage: number;\n  emptyMessage: string;\n  items: ActivityLogItem[];\n  onPageChange: (page: number) => void;\n  pageNumbers: number[];\n  pageSize: number;\n  totalPages: number;\n}) {\n  const visibleItems = useMemo(() => {\n    const start = (currentPage - 1) * pageSize;\n    return items.slice(start, start + pageSize);\n  }, [currentPage, items, pageSize]);\n\n  if (items.length === 0) {\n    return (\n      <div className=\"rounded-lg border border-dashed px-4 py-8 text-center text-sm text-muted-foreground\">\n        {emptyMessage}\n      </div>\n    );\n  }\n\n  return (\n    <>\n      <ScrollArea className=\"max-h-[26rem] pr-4\">\n        <ol className=\"space-y-4 pb-2\">\n          {visibleItems.map((item) => (\n            <ActivityRow item={item} key={item.id} />\n          ))}\n        </ol>\n      </ScrollArea>\n      <Separator />\n      <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n        <p className=\"text-sm text-muted-foreground\">\n          Showing {(currentPage - 1) * pageSize + 1}\n          {\" - \"}\n          {(currentPage - 1) * pageSize + visibleItems.length} of {items.length}\n        </p>\n        <PaginationControls\n          currentPage={currentPage}\n          onPageChange={onPageChange}\n          pageNumbers={pageNumbers}\n          totalPages={totalPages}\n        />\n      </div>\n    </>\n  );\n}\n\nconst ActivityLog = forwardRef<HTMLDivElement, ActivityLogProps>(\n  (\n    {\n      className,\n      defaultPage = 1,\n      description,\n      emptyMessage = \"No activity recorded yet.\",\n      items,\n      onPageChange,\n      page,\n      pageSize = 5,\n      title = \"Activity log\",\n      ...props\n    },\n    ref,\n  ) => {\n    const totalPages = Math.max(1, Math.ceil(items.length / pageSize));\n    const [uncontrolledPage, setUncontrolledPage] = useState(defaultPage);\n    const currentPage = Math.min(\n      Math.max(page ?? uncontrolledPage, 1),\n      totalPages,\n    );\n    const pageNumbers = useMemo(\n      () => buildPageNumbers(currentPage, totalPages),\n      [currentPage, totalPages],\n    );\n\n    function handlePageChange(nextPage: number) {\n      if (page === undefined) {\n        setUncontrolledPage(nextPage);\n      }\n      onPageChange?.(nextPage);\n    }\n\n    return (\n      <Card className={cn(\"w-full\", className)} ref={ref} {...props}>\n        <ActivityLogHeader\n          currentPage={currentPage}\n          description={description}\n          title={title}\n          totalPages={totalPages}\n        />\n        <CardContent className=\"space-y-4\">\n          <ActivityLogBody\n            currentPage={currentPage}\n            emptyMessage={emptyMessage}\n            items={items}\n            onPageChange={handlePageChange}\n            pageNumbers={pageNumbers}\n            pageSize={pageSize}\n            totalPages={totalPages}\n          />\n        </CardContent>\n      </Card>\n    );\n  },\n);\n\nActivityLog.displayName = \"ActivityLog\";\n\nexport { ActivityLog };\n",
      "type": "registry:component"
    }
  ],
  "version": "0.2.1",
  "stability": "stable"
}
