{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "audit-log",
  "title": "Audit Log",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-react/blocks/audit-log/AuditLog.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport { Download, Fingerprint, RadioTower, Search } from 'lucide-react'\nimport { Avatar, AvatarFallback } from '@/components/ui/avatar'\nimport { Badge, type BadgeVariants } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { Input } from '@/components/ui/input'\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'\nimport { RelativeTime } from '@/components/ui/relative-time'\nimport { SectionCard } from '@/components/ui/section-card'\n\ntype AuditAction = 'sign-in' | 'user.updated' | 'role.changed' | 'export' | 'delete'\n\nexport interface AuditEntry {\n  id: string\n  actor: string\n  action: AuditAction\n  target: string\n  ip: string\n  date: Date\n}\n\nexport interface AuditLogProps {\n  entries?: AuditEntry[]\n  className?: string\n}\n\nconst actionTone: Record<AuditAction, NonNullable<BadgeVariants['variant']>> = {\n  'sign-in': 'secondary',\n  'user.updated': 'default',\n  'role.changed': 'warning',\n  export: 'info',\n  delete: 'destructive',\n}\n\nconst now = Date.now()\nconst minutesAgo = (m: number) => new Date(now - m * 60_000)\nconst hoursAgo = (h: number) => new Date(now - h * 3_600_000)\nconst daysAgo = (d: number) => new Date(now - d * 86_400_000)\n\nconst stubEntries: AuditEntry[] = [\n  {\n    id: 'a1',\n    actor: 'Amara Osei',\n    action: 'role.changed',\n    target: 'priya@acme.com → Editor',\n    ip: '77.12.44.9',\n    date: minutesAgo(4),\n  },\n  { id: 'a2', actor: 'System', action: 'export', target: 'payroll-2026-07.csv', ip: '—', date: minutesAgo(38) },\n  {\n    id: 'a3',\n    actor: 'Jonas Weber',\n    action: 'sign-in',\n    target: 'admin.acme.com',\n    ip: '84.190.201.3',\n    date: hoursAgo(1),\n  },\n  {\n    id: 'a4',\n    actor: 'Priya Nair',\n    action: 'user.updated',\n    target: 'profile.phone',\n    ip: '103.25.8.77',\n    date: hoursAgo(3),\n  },\n  {\n    id: 'a5',\n    actor: 'Amara Osei',\n    action: 'delete',\n    target: 'draft-policy-v3.pdf',\n    ip: '77.12.44.9',\n    date: hoursAgo(5),\n  },\n  { id: 'a6', actor: 'Marcus Lee', action: 'sign-in', target: 'admin.acme.com', ip: '45.62.110.204', date: daysAgo(1) },\n  {\n    id: 'a7',\n    actor: 'Priya Nair',\n    action: 'user.updated',\n    target: 'team/onboarding-flow',\n    ip: '103.25.8.77',\n    date: daysAgo(1),\n  },\n  { id: 'a8', actor: 'System', action: 'role.changed', target: 'contractors → Viewer', ip: '—', date: daysAgo(2) },\n  {\n    id: 'a9',\n    actor: 'Jonas Weber',\n    action: 'export',\n    target: 'audit-trail-q2.json',\n    ip: '84.190.201.3',\n    date: daysAgo(2),\n  },\n  {\n    id: 'a10',\n    actor: 'Marcus Lee',\n    action: 'delete',\n    target: 'staging-deploy-key',\n    ip: '45.62.110.204',\n    date: daysAgo(3),\n  },\n]\n\nfunction initials(name: string) {\n  return name\n    .split(' ')\n    .map((part) => part[0])\n    .slice(0, 2)\n    .join('')\n    .toUpperCase()\n}\n\nexport function AuditLog({ entries, className }: AuditLogProps) {\n  const [search, setSearch] = React.useState('')\n  const [actionFilter, setActionFilter] = React.useState<'all' | AuditAction>('all')\n\n  const source = entries ?? stubEntries\n  const filtered = source.filter((entry) => {\n    const matchesQuery = search === '' || `${entry.actor} ${entry.target}`.toLowerCase().includes(search.toLowerCase())\n    const matchesAction = actionFilter === 'all' || entry.action === actionFilter\n    return matchesQuery && matchesAction\n  })\n\n  return (\n    <SectionCard\n      data-slot=\"audit-log\"\n      title=\"Audit log\"\n      description=\"Who changed what, when — across your workspace.\"\n      className={className}\n      headerAction={\n        <span className=\"text-success flex items-center gap-1.5 text-xs font-medium\">\n          <RadioTower className=\"size-3.5\" aria-hidden=\"true\" />\n          live\n        </span>\n      }\n    >\n      <div className=\"flex flex-col gap-2 sm:flex-row sm:items-center\">\n        <div className=\"relative flex-1\">\n          <Search\n            className=\"text-muted-foreground pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2\"\n            aria-hidden=\"true\"\n          />\n          <Input\n            value={search}\n            onChange={(e) => setSearch(e.target.value)}\n            placeholder=\"Search actor or target…\"\n            className=\"pl-8\"\n          />\n        </div>\n        <Select value={actionFilter} onValueChange={(value) => setActionFilter(value as 'all' | AuditAction)}>\n          <SelectTrigger className=\"sm:w-44\">\n            <SelectValue placeholder=\"All actions\" />\n          </SelectTrigger>\n          <SelectContent>\n            <SelectItem value=\"all\">All actions</SelectItem>\n            {(Object.keys(actionTone) as AuditAction[]).map((action) => (\n              <SelectItem key={action} value={action}>\n                {action}\n              </SelectItem>\n            ))}\n          </SelectContent>\n        </Select>\n        <Button aria-label=\"Download attachment\" variant=\"outline\" size=\"sm\">\n          <Download aria-hidden=\"true\" />\n          Export\n        </Button>\n      </div>\n\n      <ul className=\"-mb-4 divide-y\">\n        {filtered.length === 0 && (\n          <li className=\"text-muted-foreground flex flex-col items-center gap-2 py-10 text-sm\">\n            <Fingerprint className=\"size-6 opacity-50\" aria-hidden=\"true\" />\n            No activity matches your filters.\n          </li>\n        )}\n        {filtered.map((entry) => (\n          <li key={entry.id} className=\"flex items-center gap-3 py-3\">\n            <Avatar className=\"size-7\">\n              <AvatarFallback className=\"text-xs\">{initials(entry.actor)}</AvatarFallback>\n            </Avatar>\n            <div className=\"min-w-0 flex-1\">\n              <div className=\"flex items-center gap-2\">\n                <p className=\"truncate text-sm font-medium\">{entry.actor}</p>\n                <Badge variant={actionTone[entry.action]} className=\"shrink-0\">\n                  {entry.action}\n                </Badge>\n              </div>\n              <p className=\"text-muted-foreground truncate font-mono text-xs\">{entry.target}</p>\n            </div>\n            <div className=\"hidden text-right sm:block\">\n              <RelativeTime date={entry.date} className=\"text-muted-foreground block text-xs\" />\n              <p className=\"text-muted-foreground/70 font-mono text-xs\">{entry.ip}</p>\n            </div>\n          </li>\n        ))}\n      </ul>\n    </SectionCard>\n  )\n}\n",
      "type": "registry:block",
      "target": "~/components/blocks/AuditLog.tsx"
    }
  ],
  "dependencies": [
    "lucide-react"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/react/avatar.json",
    "https://uipkge.dev/r/react/badge.json",
    "https://uipkge.dev/r/react/button.json",
    "https://uipkge.dev/r/react/input.json",
    "https://uipkge.dev/r/react/relative-time.json",
    "https://uipkge.dev/r/react/section-card.json",
    "https://uipkge.dev/r/react/select.json"
  ],
  "description": "Admin activity trail in a SectionCard: live indicator, search across actor/target, action-type filter, and rows pairing an avatar actor with a color-coded action badge, monospace target, IP and relative timestamp. Filtering is functional against the `entries` prop; swap the stub data for your source.",
  "categories": [
    "legal",
    "dashboard",
    "data"
  ]
}