{
  "$schema": "https://abackui.com/schema/registry-item.json",
  "name": "users-table",
  "type": "registry:block",
  "title": "Users table",
  "description": "A searchable member directory with role filters, sorting, row actions, empty state, and pagination.",
  "dependencies": [
    "@tabler/icons-react"
  ],
  "registryDependencies": [
    "https://abackui.com/r/utils.json",
    "https://abackui.com/r/avatar.json",
    "https://abackui.com/r/badge.json",
    "https://abackui.com/r/button.json",
    "https://abackui.com/r/dropdown-menu.json",
    "https://abackui.com/r/empty-state.json",
    "https://abackui.com/r/input.json",
    "https://abackui.com/r/pagination.json",
    "https://abackui.com/r/table.json"
  ],
  "categories": [
    "Data Display"
  ],
  "docs": "https://abackui.com/docs/blocks/users-table",
  "meta": {
    "status": "beta"
  },
  "files": [
    {
      "path": "components/blocks/users-table.tsx",
      "content": "\"use client\";\n\nimport { useMemo, useState } from \"react\";\nimport {\n  IconArrowsSort,\n  IconChevronDown,\n  IconDots,\n  IconSearch,\n  IconUsers,\n} from \"@tabler/icons-react\";\n\nimport { Avatar, AvatarFallback } from \"@/components/ui/avatar\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { EmptyState } from \"@/components/ui/empty-state\";\nimport { Input } from \"@/components/ui/input\";\nimport { Pagination } from \"@/components/ui/pagination\";\nimport {\n  Table,\n  TableBody,\n  TableCaption,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"@/components/ui/table\";\n\nexport type MemberRole = \"Admin\" | \"Editor\" | \"Viewer\";\nexport type MemberStatus = \"active\" | \"invited\" | \"suspended\";\n\nexport interface WorkspaceMember {\n  id: string;\n  name: string;\n  email: string;\n  role: MemberRole;\n  status: MemberStatus;\n  lastActive: string;\n}\n\nexport interface UsersTableBlockProps {\n  members?: WorkspaceMember[];\n  /** Renders an invite action in the toolbar when provided. */\n  onInvite?: () => void;\n  /** Called when a row's manage action is chosen. */\n  onManage?: (member: WorkspaceMember) => void;\n}\n\nconst DEMO_MEMBERS: WorkspaceMember[] = [\n  { id: \"1\", name: \"Aditi Rao\", email: \"aditi@aback.com\", role: \"Admin\", status: \"active\", lastActive: \"2 minutes ago\" },\n  { id: \"2\", name: \"Bilal Khan\", email: \"bilal@aback.com\", role: \"Editor\", status: \"active\", lastActive: \"1 hour ago\" },\n  { id: \"3\", name: \"Chen Wei\", email: \"chen@aback.com\", role: \"Viewer\", status: \"invited\", lastActive: \"Invite sent\" },\n  { id: \"4\", name: \"Dara Okafor\", email: \"dara@aback.com\", role: \"Editor\", status: \"active\", lastActive: \"Yesterday\" },\n  { id: \"5\", name: \"Elena Rossi\", email: \"elena@aback.com\", role: \"Viewer\", status: \"suspended\", lastActive: \"3 weeks ago\" },\n  { id: \"6\", name: \"Farid Haddad\", email: \"farid@aback.com\", role: \"Editor\", status: \"active\", lastActive: \"3 days ago\" },\n  { id: \"7\", name: \"Grace Lee\", email: \"grace@aback.com\", role: \"Admin\", status: \"active\", lastActive: \"5 minutes ago\" },\n  { id: \"8\", name: \"Hassan Ali\", email: \"hassan@aback.com\", role: \"Viewer\", status: \"invited\", lastActive: \"Invite sent\" },\n  { id: \"9\", name: \"Imani Brooks\", email: \"imani@aback.com\", role: \"Viewer\", status: \"active\", lastActive: \"1 week ago\" },\n  { id: \"10\", name: \"Jonas Weber\", email: \"jonas@aback.com\", role: \"Editor\", status: \"active\", lastActive: \"4 hours ago\" },\n  { id: \"11\", name: \"Keiko Tanaka\", email: \"keiko@aback.com\", role: \"Viewer\", status: \"active\", lastActive: \"2 days ago\" },\n];\n\nconst PAGE_SIZE = 5;\nconst ROLES: Array<\"all\" | MemberRole> = [\"all\", \"Admin\", \"Editor\", \"Viewer\"];\n\nconst statusVariant = {\n  active: \"success\",\n  invited: \"warning\",\n  suspended: \"outline\",\n} as const;\n\nfunction initials(name: string) {\n  return name\n    .split(\" \")\n    .map((part) => part[0])\n    .filter(Boolean)\n    .slice(0, 2)\n    .join(\"\");\n}\n\nexport function UsersTableBlock({\n  members = DEMO_MEMBERS,\n  onInvite,\n  onManage,\n}: UsersTableBlockProps) {\n  const [query, setQuery] = useState(\"\");\n  const [role, setRole] = useState<\"all\" | MemberRole>(\"all\");\n  const [sort, setSort] = useState<\"asc\" | \"desc\">(\"asc\");\n  const [page, setPage] = useState(1);\n  const [copied, setCopied] = useState<string | null>(null);\n\n  const filtered = useMemo(() => {\n    const term = query.trim().toLowerCase();\n    return members\n      .filter((member) => {\n        const matchesQuery =\n          !term ||\n          `${member.name} ${member.email}`.toLowerCase().includes(term);\n        const matchesRole = role === \"all\" || member.role === role;\n        return matchesQuery && matchesRole;\n      })\n      .sort((a, b) =>\n        sort === \"asc\" ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name),\n      );\n  }, [members, query, role, sort]);\n\n  const pageCount = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));\n  const current = Math.min(page, pageCount);\n  const visible = filtered.slice((current - 1) * PAGE_SIZE, current * PAGE_SIZE);\n\n  function resetFilters() {\n    setQuery(\"\");\n    setRole(\"all\");\n    setPage(1);\n  }\n\n  return (\n    <section aria-labelledby=\"members-title\" className=\"flex flex-col gap-4\">\n      <div className=\"flex flex-wrap items-end justify-between gap-3\">\n        <div>\n          <h2 id=\"members-title\" className=\"text-lg font-semibold tracking-tight text-foreground\">\n            Members\n          </h2>\n          <p className=\"mt-0.5 text-sm text-muted-foreground\">\n            {members.length} people have access to this workspace.\n          </p>\n        </div>\n        {onInvite ? (\n          <Button onClick={onInvite}>\n            <IconUsers aria-hidden=\"true\" />\n            Invite member\n          </Button>\n        ) : null}\n      </div>\n\n      <div className=\"flex flex-wrap items-center gap-2\">\n        <div className=\"relative min-w-0 flex-1 sm:max-w-xs\">\n          <IconSearch\n            aria-hidden=\"true\"\n            className=\"pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground\"\n          />\n          <Input\n            aria-label=\"Search members\"\n            placeholder=\"Search name or email\"\n            value={query}\n            onChange={(event) => {\n              setQuery(event.target.value);\n              setPage(1);\n            }}\n            className=\"pl-8\"\n          />\n        </div>\n\n        <DropdownMenu>\n          <DropdownMenuTrigger asChild>\n            <Button variant=\"outline\" size=\"sm\">\n              Role: {role === \"all\" ? \"All\" : role}\n              <IconChevronDown aria-hidden=\"true\" />\n            </Button>\n          </DropdownMenuTrigger>\n          <DropdownMenuContent align=\"start\">\n            {ROLES.map((option) => (\n              <DropdownMenuItem\n                key={option}\n                onSelect={() => {\n                  setRole(option);\n                  setPage(1);\n                }}\n              >\n                {option === \"all\" ? \"All roles\" : option}\n              </DropdownMenuItem>\n            ))}\n          </DropdownMenuContent>\n        </DropdownMenu>\n\n        <Button\n          variant=\"outline\"\n          size=\"sm\"\n          onClick={() => setSort((currentSort) => (currentSort === \"asc\" ? \"desc\" : \"asc\"))}\n          aria-label={`Sort by name, ${sort === \"asc\" ? \"ascending\" : \"descending\"}`}\n        >\n          <IconArrowsSort aria-hidden=\"true\" />\n          Name {sort === \"asc\" ? \"A–Z\" : \"Z–A\"}\n        </Button>\n      </div>\n\n      <p role=\"status\" className=\"sr-only\">\n        {copied ? `Copied ${copied} to the clipboard.` : \"\"}\n      </p>\n\n      {filtered.length === 0 ? (\n        <EmptyState\n          icon={<IconSearch aria-hidden=\"true\" />}\n          title=\"No members match\"\n          description=\"Try a different name, email, or role.\"\n          action={\n            <Button variant=\"outline\" size=\"sm\" onClick={resetFilters}>\n              Clear filters\n            </Button>\n          }\n        />\n      ) : (\n        <>\n          <Table containerProps={{ \"aria-label\": \"Workspace members\" }}>\n            <TableCaption className=\"sr-only\">\n              Workspace members, their role, status, and last activity.\n            </TableCaption>\n            <TableHeader>\n              <TableRow>\n                <TableHead scope=\"col\">Member</TableHead>\n                <TableHead scope=\"col\">Role</TableHead>\n                <TableHead scope=\"col\">Status</TableHead>\n                <TableHead scope=\"col\">Last active</TableHead>\n                <TableHead scope=\"col\" className=\"w-10\">\n                  <span className=\"sr-only\">Actions</span>\n                </TableHead>\n              </TableRow>\n            </TableHeader>\n            <TableBody>\n              {visible.map((member) => (\n                <TableRow key={member.id}>\n                  <TableCell>\n                    <div className=\"flex items-center gap-3\">\n                      <Avatar size=\"sm\">\n                        <AvatarFallback>{initials(member.name)}</AvatarFallback>\n                      </Avatar>\n                      <div className=\"min-w-0\">\n                        <p className=\"truncate text-sm font-medium text-foreground\">\n                          {member.name}\n                        </p>\n                        <p className=\"truncate text-xs text-muted-foreground\">{member.email}</p>\n                      </div>\n                    </div>\n                  </TableCell>\n                  <TableCell className=\"text-sm text-muted-foreground\">{member.role}</TableCell>\n                  <TableCell>\n                    <Badge variant={statusVariant[member.status]}>{member.status}</Badge>\n                  </TableCell>\n                  <TableCell className=\"whitespace-nowrap text-xs text-muted-foreground\">\n                    {member.lastActive}\n                  </TableCell>\n                  <TableCell>\n                    <DropdownMenu>\n                      <DropdownMenuTrigger asChild>\n                        <Button variant=\"ghost\" size=\"icon-sm\" aria-label={`Actions for ${member.name}`}>\n                          <IconDots aria-hidden=\"true\" />\n                        </Button>\n                      </DropdownMenuTrigger>\n                      <DropdownMenuContent align=\"end\">\n                        <DropdownMenuItem\n                          onSelect={() => {\n                            void navigator.clipboard?.writeText(member.email);\n                            setCopied(member.email);\n                          }}\n                        >\n                          Copy email\n                        </DropdownMenuItem>\n                        {onManage ? (\n                          <>\n                            <DropdownMenuSeparator />\n                            <DropdownMenuItem onSelect={() => onManage(member)}>\n                              Manage member\n                            </DropdownMenuItem>\n                          </>\n                        ) : null}\n                      </DropdownMenuContent>\n                    </DropdownMenu>\n                  </TableCell>\n                </TableRow>\n              ))}\n            </TableBody>\n          </Table>\n\n          <div className=\"flex flex-wrap items-center justify-between gap-3\">\n            <p className=\"text-xs text-muted-foreground\">\n              Showing {visible.length} of {filtered.length} members\n            </p>\n            <Pagination\n              page={current}\n              pageCount={pageCount}\n              onPageChange={setPage}\n              labels={{ navigation: \"Member pages\" }}\n            />\n          </div>\n        </>\n      )}\n    </section>\n  );\n}\n",
      "type": "registry:block",
      "target": "components/blocks/users-table.tsx"
    }
  ]
}
