{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "audit-log",
  "title": "Audit Log",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-vue/blocks/audit-log/AuditLog.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport type { HTMLAttributes } from 'vue'\nimport { Download, Fingerprint, RadioTower, Search } from 'lucide-vue-next'\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\ninterface AuditEntry {\n  id: string\n  actor: string\n  action: AuditAction\n  target: string\n  ip: string\n  date: Date\n}\n\nconst props = defineProps<{\n  entries?: AuditEntry[]\n  class?: HTMLAttributes['class']\n}>()\n\nconst actionTone: Record<AuditAction, BadgeVariants['variant']> = {\n  'sign-in': 'secondary',\n  'user.updated': 'default',\n  'role.changed': 'warning',\n  export: 'info',\n  delete: 'destructive',\n}\n\nconst now = new Date()\nconst minutesAgo = (m: number) => new Date(now.getTime() - m * 60_000)\nconst hoursAgo = (h: number) => new Date(now.getTime() - h * 3_600_000)\nconst daysAgo = (d: number) => new Date(now.getTime() - 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\nconst entries = computed(() => props.entries ?? stubEntries)\n\nconst search = ref('')\nconst actionFilter = ref<'all' | AuditAction>('all')\n\nconst filtered = computed(() =>\n  entries.value.filter((entry) => {\n    const matchesQuery =\n      search.value === '' || `${entry.actor} ${entry.target}`.toLowerCase().includes(search.value.toLowerCase())\n    const matchesAction = actionFilter.value === 'all' || entry.action === actionFilter.value\n    return matchesQuery && matchesAction\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</script>\n\n<template>\n  <SectionCard\n    data-slot=\"audit-log\"\n    title=\"Audit log\"\n    description=\"Who changed what, when — across your workspace.\"\n    :class=\"props.class\"\n  >\n    <template #header-action>\n      <span class=\"text-success flex items-center gap-1.5 text-xs font-medium\">\n        <RadioTower class=\"size-3.5\" aria-hidden=\"true\" />\n        live\n      </span>\n    </template>\n\n    <div class=\"flex flex-col gap-2 sm:flex-row sm:items-center\">\n      <div class=\"relative flex-1\">\n        <Search\n          class=\"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 v-model=\"search\" placeholder=\"Search actor or target…\" class=\"pl-8\" />\n      </div>\n      <Select v-model=\"actionFilter\">\n        <SelectTrigger class=\"sm:w-44\">\n          <SelectValue placeholder=\"All actions\" />\n        </SelectTrigger>\n        <SelectContent>\n          <SelectItem value=\"all\">All actions</SelectItem>\n          <SelectItem v-for=\"(tone, action) in actionTone\" :key=\"action\" :value=\"action\">{{ action }}</SelectItem>\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 class=\"-mb-4 divide-y\">\n      <li v-if=\"filtered.length === 0\" class=\"text-muted-foreground flex flex-col items-center gap-2 py-10 text-sm\">\n        <Fingerprint class=\"size-6 opacity-50\" aria-hidden=\"true\" />\n        No activity matches your filters.\n      </li>\n      <li v-for=\"entry in filtered\" :key=\"entry.id\" class=\"flex items-center gap-3 py-3\">\n        <Avatar class=\"size-7\">\n          <AvatarFallback class=\"text-xs\">{{ initials(entry.actor) }}</AvatarFallback>\n        </Avatar>\n        <div class=\"min-w-0 flex-1\">\n          <div class=\"flex items-center gap-2\">\n            <p class=\"truncate text-sm font-medium\">{{ entry.actor }}</p>\n            <Badge :variant=\"actionTone[entry.action]\" class=\"shrink-0\">{{ entry.action }}</Badge>\n          </div>\n          <p class=\"text-muted-foreground truncate font-mono text-xs\">{{ entry.target }}</p>\n        </div>\n        <div class=\"hidden text-right sm:block\">\n          <RelativeTime :date=\"entry.date\" class=\"text-muted-foreground block text-xs\" />\n          <p class=\"text-muted-foreground/70 font-mono text-xs\">{{ entry.ip }}</p>\n        </div>\n      </li>\n    </ul>\n  </SectionCard>\n</template>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/AuditLog.vue"
    }
  ],
  "dependencies": [
    "lucide-vue-next"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/vue/avatar.json",
    "https://uipkge.dev/r/vue/badge.json",
    "https://uipkge.dev/r/vue/button.json",
    "https://uipkge.dev/r/vue/input.json",
    "https://uipkge.dev/r/vue/relative-time.json",
    "https://uipkge.dev/r/vue/section-card.json",
    "https://uipkge.dev/r/vue/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"
  ]
}