{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "role-matrix",
  "title": "Role Matrix",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-react/blocks/role-matrix/RoleMatrix.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport { ShieldCheck } from 'lucide-react'\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\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 interface RoleMatrixProps {\n  roles?: Role[]\n  groups?: PermissionGroup[]\n  /** permissionId -> array of granted roleIds. */\n  value?: Record<string, string[]>\n  onValueChange?: (value: Record<string, string[]>) => void\n  readOnly?: boolean\n  className?: string\n}\n\nconst defaultRoles: Role[] = [\n  { id: 'admin', label: 'Admin' },\n  { id: 'editor', label: 'Editor' },\n  { id: 'viewer', label: 'Viewer' },\n]\n\nconst 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\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\nexport function RoleMatrix({\n  roles = defaultRoles,\n  groups = defaultGroups,\n  value,\n  onValueChange,\n  readOnly = false,\n  className,\n}: RoleMatrixProps) {\n  // Internal working copy keeps the block self-contained; consumers can pass\n  // value/onValueChange to mirror changes out (we never mutate props directly).\n  const [internalGrants, setInternalGrants] = React.useState<Record<string, string[]>>(\n    value ? { ...value } : { ...defaultGrants },\n  )\n  const grants = internalGrants\n  const allRoleIds = roles.map((r) => r.id)\n\n  function setGrant(permissionId: string, roleIds: string[]) {\n    const next = { ...grants, [permissionId]: roleIds }\n    setInternalGrants(next)\n    onValueChange?.(next)\n  }\n\n  function isGranted(permissionId: string, roleId: string) {\n    return (grants[permissionId] ?? []).includes(roleId)\n  }\n\n  function toggleCell(permissionId: string, roleId: string) {\n    if (readOnly) return\n    const current = grants[permissionId] ?? []\n    setGrant(permissionId, current.includes(roleId) ? current.filter((id) => id !== roleId) : [...current, roleId])\n  }\n\n  function roleState(roleId: string): boolean | 'indeterminate' {\n    const total = groups.reduce((n, g) => n + g.permissions.length, 0)\n    const count = 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\n  function toggleRoleColumn(roleId: string) {\n    if (readOnly) return\n    const turnOn = roleState(roleId) !== true\n    const next = { ...grants }\n    for (const group of groups) {\n      for (const permission of group.permissions) {\n        const current = next[permission.id] ?? []\n        const has = current.includes(roleId)\n        next[permission.id] = turnOn\n          ? has\n            ? current\n            : [...current, roleId]\n          : has\n            ? current.filter((id) => id !== roleId)\n            : current\n      }\n    }\n    setInternalGrants(next)\n    onValueChange?.(next)\n  }\n\n  function groupSummary(group: PermissionGroup) {\n    const total = group.permissions.length * allRoleIds.length\n    const granted = group.permissions.reduce(\n      (n, p) => n + allRoleIds.filter((roleId) => isGranted(p.id, roleId)).length,\n      0,\n    )\n    return `${granted} of ${total} grants`\n  }\n\n  return (\n    <SectionCard\n      data-slot=\"role-matrix\"\n      title=\"Roles & permissions\"\n      description=\"What each role can do, grouped by resource.\"\n      className={className}\n      headerAction={\n        readOnly ? (\n          <Badge variant=\"secondary\">\n            <ShieldCheck aria-hidden=\"true\" />\n            Read-only\n          </Badge>\n        ) : undefined\n      }\n    >\n      <div className=\"-mx-6 overflow-x-auto px-6\">\n        <Table className=\"max-w-[480px] min-w-full\">\n          <TableHeader>\n            <TableRow>\n              <TableHead className=\"w-[46%]\">Permission</TableHead>\n              {roles.map((role) => (\n                <TableHead key={role.id} className=\"text-center\">\n                  <span className=\"block text-xs font-medium\">{role.label}</span>\n                  <Checkbox\n                    checked={roleState(role.id)}\n                    disabled={readOnly}\n                    size=\"sm\"\n                    aria-label={`Toggle all ${role.label} permissions`}\n                    className=\"mx-auto mt-1 block\"\n                    onCheckedChange={() => toggleRoleColumn(role.id)}\n                  />\n                </TableHead>\n              ))}\n            </TableRow>\n          </TableHeader>\n          <TableBody>\n            {groups.map((group) => (\n              <React.Fragment key={group.resource}>\n                <TableRow className=\"hover:bg-transparent\">\n                  <TableCell colSpan={roles.length + 1} className=\"bg-muted/40 h-10 py-2\">\n                    <div className=\"flex items-center justify-between gap-x-2\">\n                      <span className=\"text-xs font-semibold tracking-wider uppercase\">{group.resource}</span>\n                      <span className=\"text-muted-foreground text-xs\">{groupSummary(group)}</span>\n                    </div>\n                  </TableCell>\n                </TableRow>\n                {group.permissions.map((permission) => (\n                  <TableRow key={permission.id}>\n                    <TableCell className=\"py-2.5 pl-4 font-normal\">{permission.label}</TableCell>\n                    {roles.map((role) => (\n                      <TableCell key={role.id} className=\"text-center\">\n                        <Checkbox\n                          checked={isGranted(permission.id, role.id)}\n                          disabled={readOnly}\n                          size=\"sm\"\n                          aria-label={`${permission.label} — ${role.label}`}\n                          className=\"mx-auto\"\n                          onCheckedChange={() => toggleCell(permission.id, role.id)}\n                        />\n                      </TableCell>\n                    ))}\n                  </TableRow>\n                ))}\n              </React.Fragment>\n            ))}\n          </TableBody>\n        </Table>\n      </div>\n\n      <p className=\"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  )\n}\n",
      "type": "registry:block",
      "target": "~/components/blocks/RoleMatrix.tsx"
    }
  ],
  "dependencies": [
    "lucide-react"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/react/badge.json",
    "https://uipkge.dev/r/react/checkbox.json",
    "https://uipkge.dev/r/react/section-card.json",
    "https://uipkge.dev/r/react/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/onValueChange mirror the full grants map; stub data covers users, billing and content.",
  "categories": [
    "security",
    "dashboard",
    "data"
  ]
}