{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "role-matrix",
  "title": "Role Matrix",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-vue/blocks/role-matrix/RoleMatrix.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport type { HTMLAttributes } from 'vue'\nimport { ShieldCheck } from 'lucide-vue-next'\nimport { Badge } from '@/components/ui/badge'\nimport { Checkbox } from '@/components/ui/checkbox'\nimport { SectionCard } from '@/components/ui/section-card'\nimport { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'\n\ninterface Role {\n  id: string\n  label: string\n}\n\ninterface Permission {\n  id: string\n  label: string\n}\n\ninterface PermissionGroup {\n  resource: string\n  permissions: Permission[]\n}\n\nconst props = withDefaults(\n  defineProps<{\n    roles?: Role[]\n    groups?: PermissionGroup[]\n    /** permissionId -> array of granted roleIds. */\n    modelValue?: Record<string, string[]>\n    readOnly?: boolean\n    class?: HTMLAttributes['class']\n  }>(),\n  {\n    roles: () => [\n      { id: 'admin', label: 'Admin' },\n      { id: 'editor', label: 'Editor' },\n      { id: 'viewer', label: 'Viewer' },\n    ],\n    groups: () => [\n      {\n        resource: 'Users',\n        permissions: [\n          { id: 'users.create', label: 'Invite members' },\n          { id: 'users.read', label: 'View members' },\n          { id: 'users.update', label: 'Edit profiles & roles' },\n          { id: 'users.delete', label: 'Remove members' },\n        ],\n      },\n      {\n        resource: 'Billing',\n        permissions: [\n          { id: 'billing.read', label: 'View invoices' },\n          { id: 'billing.manage', label: 'Change plan & seats' },\n        ],\n      },\n      {\n        resource: 'Content',\n        permissions: [\n          { id: 'content.create', label: 'Create documents' },\n          { id: 'content.read', label: 'Read documents' },\n          { id: 'content.delete', label: 'Delete documents' },\n        ],\n      },\n    ],\n    modelValue: undefined,\n    readOnly: false,\n  },\n)\n\nconst defaultGrants: Record<string, string[]> = {\n  'users.create': ['admin'],\n  'users.read': ['admin', 'editor', 'viewer'],\n  'users.update': ['admin'],\n  'users.delete': ['admin'],\n  'billing.read': ['admin', 'editor'],\n  'billing.manage': ['admin'],\n  'content.create': ['admin', 'editor'],\n  'content.read': ['admin', 'editor', 'viewer'],\n  'content.delete': ['admin', 'editor'],\n}\n\nconst emit = defineEmits<{ 'update:modelValue': [Record<string, string[]>] }>()\n\n// Internal working copy keeps the block self-contained; consumers can pass\n// v-model to mirror changes out (we never mutate props directly).\nconst internalGrants = ref<Record<string, string[]>>(props.modelValue ? { ...props.modelValue } : { ...defaultGrants })\nconst grants = computed(() => internalGrants.value)\nconst allRoleIds = computed(() => props.roles.map((r) => r.id))\n\nfunction setGrant(permissionId: string, roleIds: string[]) {\n  internalGrants.value = { ...internalGrants.value, [permissionId]: roleIds }\n  if (props.modelValue !== undefined) emit('update:modelValue', { ...internalGrants.value })\n}\n\nfunction isGranted(permissionId: string, roleId: string) {\n  return (grants.value[permissionId] ?? []).includes(roleId)\n}\n\nfunction toggleCell(permissionId: string, roleId: string) {\n  if (props.readOnly) return\n  const current = grants.value[permissionId] ?? []\n  setGrant(permissionId, current.includes(roleId) ? current.filter((id) => id !== roleId) : [...current, roleId])\n}\n\nfunction roleState(roleId: string): boolean | 'indeterminate' {\n  const total = props.groups.reduce((n, g) => n + g.permissions.length, 0)\n  const count = props.groups.reduce((n, g) => n + g.permissions.filter((p) => isGranted(p.id, roleId)).length, 0)\n  if (count === 0) return false\n  if (count === total) return true\n  return 'indeterminate'\n}\n\nfunction toggleRoleColumn(roleId: string) {\n  if (props.readOnly) return\n  const turnOn = roleState(roleId) !== true\n  for (const group of props.groups) {\n    for (const permission of group.permissions) {\n      const current = grants.value[permission.id] ?? []\n      const has = current.includes(roleId)\n      if (turnOn && !has) setGrant(permission.id, [...current, roleId])\n      if (!turnOn && has)\n        setGrant(\n          permission.id,\n          current.filter((id) => id !== roleId),\n        )\n    }\n  }\n}\n\nfunction groupSummary(group: PermissionGroup) {\n  const total = group.permissions.length * allRoleIds.value.length\n  const granted = group.permissions.reduce(\n    (n, p) => n + allRoleIds.value.filter((roleId) => isGranted(p.id, roleId)).length,\n    0,\n  )\n  return `${granted} of ${total} grants`\n}\n</script>\n\n<template>\n  <SectionCard\n    data-slot=\"role-matrix\"\n    title=\"Roles & permissions\"\n    description=\"What each role can do, grouped by resource.\"\n    :class=\"props.class\"\n  >\n    <template #header-action>\n      <Badge v-if=\"readOnly\" variant=\"secondary\">\n        <ShieldCheck aria-hidden=\"true\" />\n        Read-only\n      </Badge>\n    </template>\n\n    <div class=\"-mx-6 overflow-x-auto px-6\">\n      <Table class=\"max-w-[480px] min-w-full\">\n        <TableHeader>\n          <TableRow>\n            <TableHead class=\"w-[46%]\">Permission</TableHead>\n            <TableHead v-for=\"role in roles\" :key=\"role.id\" class=\"text-center\">\n              <span class=\"block text-xs font-medium\">{{ role.label }}</span>\n              <Checkbox\n                :model-value=\"roleState(role.id)\"\n                :disabled=\"readOnly\"\n                size=\"sm\"\n                :aria-label=\"`Toggle all ${role.label} permissions`\"\n                class=\"mx-auto mt-1 block\"\n                @update:model-value=\"toggleRoleColumn(role.id)\"\n              />\n            </TableHead>\n          </TableRow>\n        </TableHeader>\n        <TableBody>\n          <TableRow v-for=\"group in groups\" :key=\"group.resource\" class=\"hover:bg-transparent\">\n            <TableCell :colspan=\"roles.length + 1\" class=\"bg-muted/40 h-10 py-2\">\n              <div class=\"flex items-center justify-between gap-x-2\">\n                <span class=\"text-xs font-semibold tracking-wider uppercase\">{{ group.resource }}</span>\n                <span class=\"text-muted-foreground text-xs\">{{ groupSummary(group) }}</span>\n              </div>\n            </TableCell>\n          </TableRow>\n          <template v-for=\"group in groups\" :key=\"group.resource + '-rows'\">\n            <TableRow v-for=\"permission in group.permissions\" :key=\"permission.id\">\n              <TableCell class=\"py-2.5 pl-4 font-normal\">{{ permission.label }}</TableCell>\n              <TableCell v-for=\"role in roles\" :key=\"role.id\" class=\"text-center\">\n                <Checkbox\n                  :model-value=\"isGranted(permission.id, role.id)\"\n                  :disabled=\"readOnly\"\n                  size=\"sm\"\n                  :aria-label=\"`${permission.label} — ${role.label}`\"\n                  class=\"mx-auto\"\n                  @update:model-value=\"toggleCell(permission.id, role.id)\"\n                />\n              </TableCell>\n            </TableRow>\n          </template>\n        </TableBody>\n      </Table>\n    </div>\n\n    <p class=\"text-muted-foreground mt-4 text-xs\">\n      Column headers toggle every permission for that role. Changes apply on save.\n    </p>\n  </SectionCard>\n</template>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/RoleMatrix.vue"
    }
  ],
  "dependencies": [
    "lucide-vue-next"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/vue/badge.json",
    "https://uipkge.dev/r/vue/checkbox.json",
    "https://uipkge.dev/r/vue/section-card.json",
    "https://uipkge.dev/r/vue/table.json"
  ],
  "description": "Roles × permissions grid grouped by resource: checkbox cells with tri-state column headers (toggle a whole role), per-resource grant summaries, horizontal scroll for many roles, and a read-only mode. v-model mirrors the full grants map; stub data covers users, billing and content.",
  "categories": [
    "security",
    "dashboard",
    "data"
  ]
}