{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "role-matrix",
  "title": "Role Matrix",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/role-matrix/role-matrix.component.ts",
      "content": "// Roles × permissions grid grouped by resource: checkbox cells with tri-state column\n// headers (toggle a whole role), per-resource grant summaries, and a read-only mode.\n// Port of the React block 1:1.\nimport { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, computed, signal } from '@angular/core'\nimport { ShieldCheck, LucideAngularModule } from 'lucide-angular'\nimport { UiBadgeComponent } from '@/ui/badge/badge.component'\nimport { UiCheckboxComponent, type CheckedState } from '@/ui/checkbox/checkbox.component'\nimport { UiSectionCardComponent } from '@/ui/section-card/section-card.component'\nimport {\n  UiTableBodyComponent,\n  UiTableCellComponent,\n  UiTableComponent,\n  UiTableHeadComponent,\n  UiTableHeaderComponent,\n  UiTableRowComponent,\n} from '@/ui/table/table.component'\n\nexport interface Role {\n  id: string\n  label: string\n}\n\nexport interface Permission {\n  id: string\n  label: string\n}\n\nexport interface PermissionGroup {\n  resource: string\n  permissions: Permission[]\n}\n\nexport const defaultRoles: Role[] = [\n  { id: 'admin', label: 'Admin' },\n  { id: 'editor', label: 'Editor' },\n  { id: 'viewer', label: 'Viewer' },\n]\n\nexport const defaultGroups: PermissionGroup[] = [\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\nexport const 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\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-role-matrix, [ui-role-matrix]',\n  standalone: true,\n  // React renders the SectionCard itself: the host stays out of layout and `class` goes to the card.\n  host: { '[attr.class]': '\"contents\"' },\n  imports: [\n    LucideAngularModule,\n    UiBadgeComponent,\n    UiCheckboxComponent,\n    UiSectionCardComponent,\n    UiTableBodyComponent,\n    UiTableCellComponent,\n    UiTableComponent,\n    UiTableHeadComponent,\n    UiTableHeaderComponent,\n    UiTableRowComponent,\n  ],\n  template: `\n    <div\n      ui-section-card\n      data-slot=\"role-matrix\"\n      title=\"Roles & permissions\"\n      description=\"What each role can do, grouped by resource.\"\n      [class]=\"className\"\n    >\n      @if (readOnlySignal()) {\n        <span slot=\"header-action\" ui-badge variant=\"secondary\">\n          <lucide-icon [img]=\"ShieldCheck\" />\n          Read-only\n        </span>\n      }\n\n      <div class=\"-mx-6 overflow-x-auto px-6\">\n        <table ui-table class=\"max-w-[480px] min-w-full\">\n          <thead ui-table-header>\n            <tr ui-table-row>\n              <th ui-table-head class=\"w-[46%]\">Permission</th>\n              @for (role of rolesSignal(); track role.id) {\n                <th ui-table-head class=\"text-center\">\n                  <span class=\"block text-xs font-medium\">{{ role.label }}</span>\n                  <ui-checkbox\n                    [checked]=\"roleState(role.id)\"\n                    [disabled]=\"readOnlySignal()\"\n                    size=\"sm\"\n                    [attr.aria-label]=\"'Toggle all ' + role.label + ' permissions'\"\n                    class=\"mx-auto mt-1 block\"\n                    (checkedChange)=\"toggleRoleColumn(role.id)\"\n                  />\n                </th>\n              }\n            </tr>\n          </thead>\n          <tbody ui-table-body>\n            @for (group of groupsSignal(); track group.resource) {\n              <tr ui-table-row class=\"hover:bg-transparent\">\n                <td ui-table-cell [attr.colspan]=\"rolesSignal().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                </td>\n              </tr>\n              @for (permission of group.permissions; track permission.id) {\n                <tr ui-table-row>\n                  <td ui-table-cell class=\"py-2.5 pl-4 font-normal\">{{ permission.label }}</td>\n                  @for (role of rolesSignal(); track role.id) {\n                    <td ui-table-cell class=\"text-center\">\n                      <ui-checkbox\n                        [checked]=\"isGranted(permission.id, role.id)\"\n                        [disabled]=\"readOnlySignal()\"\n                        size=\"sm\"\n                        [attr.aria-label]=\"permission.label + ' — ' + role.label\"\n                        class=\"mx-auto\"\n                        (checkedChange)=\"toggleCell(permission.id, role.id)\"\n                      />\n                    </td>\n                  }\n                </tr>\n              }\n            }\n          </tbody>\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    </div>\n  `,\n})\nexport class UiRoleMatrixComponent {\n  protected readonly ShieldCheck = ShieldCheck\n\n  @Input() set roles(v: Role[] | undefined) {\n    this.rolesSignal.set(v ?? defaultRoles)\n  }\n  @Input() set groups(v: PermissionGroup[] | undefined) {\n    this.groupsSignal.set(v ?? defaultGroups)\n  }\n  @Input() set value(v: Record<string, string[]> | undefined) {\n    this.internalGrants.set(v ? { ...v } : { ...defaultGrants })\n  }\n  @Input() set readOnly(v: boolean) {\n    this.readOnlySignal.set(!!v)\n  }\n  @Input('class') className?: string\n  @Output() valueChange = new EventEmitter<Record<string, string[]>>()\n\n  readonly rolesSignal = signal<Role[]>([...defaultRoles])\n  readonly groupsSignal = signal<PermissionGroup[]>([...defaultGroups])\n  readonly readOnlySignal = signal(false)\n  readonly internalGrants = signal<Record<string, string[]>>({ ...defaultGrants })\n\n  readonly grants = computed(() => this.internalGrants())\n\n  readonly allRoleIds = computed(() => this.rolesSignal().map((r) => r.id))\n\n  setGrant(permissionId: string, roleIds: string[]): void {\n    const next = { ...this.internalGrants(), [permissionId]: roleIds }\n    this.internalGrants.set(next)\n    this.valueChange.emit({ ...next })\n  }\n\n  isGranted(permissionId: string, roleId: string): boolean {\n    return (this.internalGrants()[permissionId] ?? []).includes(roleId)\n  }\n\n  toggleCell(permissionId: string, roleId: string): void {\n    if (this.readOnlySignal()) return\n    const current = this.internalGrants()[permissionId] ?? []\n    this.setGrant(permissionId, current.includes(roleId) ? current.filter((id) => id !== roleId) : [...current, roleId])\n  }\n\n  roleState(roleId: string): CheckedState {\n    const gs = this.groupsSignal()\n    const total = gs.reduce((n, g) => n + g.permissions.length, 0)\n    const count = gs.reduce((n, g) => n + g.permissions.filter((p) => this.isGranted(p.id, roleId)).length, 0)\n    if (count === 0) return false\n    if (count === total) return true\n    return 'indeterminate'\n  }\n\n  toggleRoleColumn(roleId: string): void {\n    if (this.readOnlySignal()) return\n    const turnOn = this.roleState(roleId) !== true\n    const next = { ...this.internalGrants() }\n    for (const group of this.groupsSignal()) {\n      for (const permission of group.permissions) {\n        const current = next[permission.id] ?? []\n        const has = current.includes(roleId)\n        if (turnOn && !has) next[permission.id] = [...current, roleId]\n        if (!turnOn && has) next[permission.id] = current.filter((id) => id !== roleId)\n      }\n    }\n    this.internalGrants.set(next)\n    this.valueChange.emit({ ...next })\n  }\n\n  groupSummary(group: PermissionGroup): string {\n    const ids = this.allRoleIds()\n    const total = group.permissions.length * ids.length\n    const granted = group.permissions.reduce(\n      (n, p) => n + ids.filter((roleId) => this.isGranted(p.id, roleId)).length,\n      0,\n    )\n    return `${granted} of ${total} grants`\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/role-matrix/role-matrix.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/role-matrix/index.ts",
      "content": "export {\n  UiRoleMatrixComponent,\n  defaultRoles,\n  defaultGroups,\n  defaultGrants,\n  type Role,\n  type Permission,\n  type PermissionGroup,\n} from './role-matrix.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/role-matrix/index.ts"
    }
  ],
  "dependencies": [
    "lucide-angular"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/badge.json",
    "https://uipkge.dev/r/angular/checkbox.json",
    "https://uipkge.dev/r/angular/section-card.json",
    "https://uipkge.dev/r/angular/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. value/valueChange mirrors the full grants map; stub data covers users, billing and content.",
  "categories": [
    "security",
    "dashboard",
    "data"
  ]
}