{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "team-member-list",
  "title": "Team Member List",
  "description": "List and manage team members with roles and permissions.",
  "registryDependencies": [
    "avatar",
    "badge",
    "button",
    "card",
    "dropdown-menu",
    "empty",
    "input-group",
    "select",
    "separator"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/team/team-member-list.tsx",
      "content": "\"use client\";\n\nimport {\n  Crown,\n  Loader2,\n  Mail,\n  MoreVertical,\n  Shield,\n  Trash2,\n  User,\n  UserCheck,\n} from \"lucide-react\";\nimport { useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  Avatar,\n  AvatarFallback,\n  AvatarImage,\n} from \"@/registry/new-york/ui/avatar\";\nimport { Badge } from \"@/registry/new-york/ui/badge\";\nimport { Button } from \"@/registry/new-york/ui/button\";\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@/registry/new-york/ui/card\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@/registry/new-york/ui/dropdown-menu\";\nimport {\n  Empty,\n  EmptyDescription,\n  EmptyHeader,\n  EmptyMedia,\n  EmptyTitle,\n} from \"@/registry/new-york/ui/empty\";\nimport {\n  InputGroup,\n  InputGroupAddon,\n  InputGroupInput,\n} from \"@/registry/new-york/ui/input-group\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/registry/new-york/ui/select\";\nimport { Separator } from \"@/registry/new-york/ui/separator\";\n\nexport interface TeamMember {\n  id: string;\n  name: string;\n  email: string;\n  avatar?: string;\n  role: \"owner\" | \"admin\" | \"member\" | \"viewer\";\n  status: \"active\" | \"invited\" | \"suspended\";\n  lastActive?: Date;\n  joinedAt: Date;\n  aiUsage?: {\n    tokens: number;\n    sessions: number;\n  };\n}\n\nexport interface TeamMemberListProps {\n  members?: TeamMember[];\n  currentUserId?: string;\n  onPromote?: (memberId: string) => Promise<void>;\n  onDemote?: (memberId: string) => Promise<void>;\n  onRemove?: (memberId: string) => Promise<void>;\n  onResendInvite?: (memberId: string) => Promise<void>;\n  className?: string;\n  showSearch?: boolean;\n  showFilters?: boolean;\n  showUsage?: boolean;\n}\n\nfunction formatDate(date: Date): string {\n  return new Intl.DateTimeFormat(\"en-US\", {\n    month: \"short\",\n    day: \"numeric\",\n    year: \"numeric\",\n  }).format(date);\n}\n\nfunction formatLastActive(date?: Date): string {\n  if (!date) return \"Never\";\n  const now = Date.now();\n  const diff = now - date.getTime();\n  const minutes = Math.floor(diff / 60_000);\n  const hours = Math.floor(minutes / 60);\n  const days = Math.floor(hours / 24);\n\n  if (minutes < 1) return \"Just now\";\n  if (minutes < 60) return `${minutes}m ago`;\n  if (hours < 24) return `${hours}h ago`;\n  if (days < 7) return `${days}d ago`;\n  return formatDate(date);\n}\n\nfunction formatNumber(num: number): string {\n  if (num >= 1_000_000) {\n    return `${(num / 1_000_000).toFixed(1)}M`;\n  }\n  if (num >= 1000) {\n    return `${(num / 1000).toFixed(1)}K`;\n  }\n  return num.toString();\n}\n\nfunction getRoleIcon(role: TeamMember[\"role\"]) {\n  switch (role) {\n    case \"owner\":\n      return Crown;\n    case \"admin\":\n      return Shield;\n    case \"member\":\n      return UserCheck;\n    case \"viewer\":\n      return User;\n    default:\n      return User;\n  }\n}\n\nfunction getRoleBadgeVariant(role: TeamMember[\"role\"]) {\n  switch (role) {\n    case \"owner\":\n      return \"default\";\n    case \"admin\":\n      return \"secondary\";\n    default:\n      return \"outline\";\n  }\n}\n\nfunction getInitials(name: string): string {\n  return name\n    .split(\" \")\n    .map((n) => n[0])\n    .join(\"\")\n    .toUpperCase()\n    .slice(0, 2);\n}\n\nexport default function TeamMemberList({\n  members = [],\n  currentUserId,\n  onPromote,\n  onDemote,\n  onRemove,\n  onResendInvite,\n  className,\n  showSearch = true,\n  showFilters = true,\n  showUsage = false,\n}: TeamMemberListProps) {\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const [roleFilter, setRoleFilter] = useState<string>(\"all\");\n  const [statusFilter, setStatusFilter] = useState<string>(\"all\");\n  const [actionLoading, setActionLoading] = useState<string | null>(null);\n\n  const filteredMembers = members.filter((member) => {\n    const matchesSearch =\n      !searchQuery.trim() ||\n      member.name.toLowerCase().includes(searchQuery.toLowerCase()) ||\n      member.email.toLowerCase().includes(searchQuery.toLowerCase());\n\n    const matchesRole = roleFilter === \"all\" || member.role === roleFilter;\n    const matchesStatus =\n      statusFilter === \"all\" || member.status === statusFilter;\n\n    return matchesSearch && matchesRole && matchesStatus;\n  });\n\n  const handleAction = async (\n    action: () => Promise<void>,\n    memberId: string\n  ) => {\n    setActionLoading(memberId);\n    try {\n      await action();\n    } finally {\n      setActionLoading(null);\n    }\n  };\n\n  return (\n    <Card className={cn(\"w-full shadow-xs\", className)}>\n      <CardHeader>\n        <div className=\"flex flex-col gap-4\">\n          <div className=\"flex flex-col gap-1\">\n            <CardTitle>Team Members</CardTitle>\n            <CardDescription>\n              {members.length} member{members.length !== 1 ? \"s\" : \"\"} in your\n              team\n            </CardDescription>\n          </div>\n          {showSearch && (\n            <InputGroup>\n              <InputGroupAddon>\n                <svg\n                  className=\"size-4\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  strokeWidth=\"2\"\n                  viewBox=\"0 0 24 24\"\n                  xmlns=\"http://www.w3.org/2000/svg\"\n                >\n                  <path\n                    d=\"M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z\"\n                    strokeLinecap=\"round\"\n                    strokeLinejoin=\"round\"\n                  />\n                </svg>\n              </InputGroupAddon>\n              <InputGroupInput\n                onChange={(e: React.ChangeEvent<HTMLInputElement>) =>\n                  setSearchQuery(e.target.value)\n                }\n                placeholder=\"Search members…\"\n                type=\"search\"\n                value={searchQuery}\n              />\n            </InputGroup>\n          )}\n          {showFilters && (\n            <div className=\"flex flex-wrap gap-2\">\n              <Select onValueChange={setRoleFilter} value={roleFilter}>\n                <SelectTrigger className=\"w-full md:w-[140px]\">\n                  <SelectValue placeholder=\"All roles\" />\n                </SelectTrigger>\n                <SelectContent>\n                  <SelectItem value=\"all\">All roles</SelectItem>\n                  <SelectItem value=\"owner\">Owner</SelectItem>\n                  <SelectItem value=\"admin\">Admin</SelectItem>\n                  <SelectItem value=\"member\">Member</SelectItem>\n                  <SelectItem value=\"viewer\">Viewer</SelectItem>\n                </SelectContent>\n              </Select>\n              <Select onValueChange={setStatusFilter} value={statusFilter}>\n                <SelectTrigger className=\"w-full md:w-[140px]\">\n                  <SelectValue placeholder=\"All statuses\" />\n                </SelectTrigger>\n                <SelectContent>\n                  <SelectItem value=\"all\">All statuses</SelectItem>\n                  <SelectItem value=\"active\">Active</SelectItem>\n                  <SelectItem value=\"invited\">Invited</SelectItem>\n                  <SelectItem value=\"suspended\">Suspended</SelectItem>\n                </SelectContent>\n              </Select>\n            </div>\n          )}\n        </div>\n      </CardHeader>\n      <CardContent>\n        {filteredMembers.length === 0 ? (\n          <Empty>\n            <EmptyHeader>\n              <EmptyMedia variant=\"icon\">\n                <User className=\"size-6\" />\n              </EmptyMedia>\n              <EmptyTitle>\n                {searchQuery || roleFilter !== \"all\" || statusFilter !== \"all\"\n                  ? \"No members found\"\n                  : \"No members yet\"}\n              </EmptyTitle>\n              <EmptyDescription>\n                {searchQuery || roleFilter !== \"all\" || statusFilter !== \"all\"\n                  ? \"Try adjusting your filters\"\n                  : \"Invite members to get started\"}\n              </EmptyDescription>\n            </EmptyHeader>\n          </Empty>\n        ) : (\n          <div className=\"flex flex-col gap-0 overflow-hidden rounded-lg border\">\n            {filteredMembers.map((member, idx) => {\n              const RoleIcon = getRoleIcon(member.role);\n              const isCurrentUser = member.id === currentUserId;\n              const canModify = !isCurrentUser && member.role !== \"owner\";\n\n              return (\n                <div key={member.id}>\n                  <div className=\"flex items-center gap-4 p-4 transition-colors hover:bg-muted/50\">\n                    <div className=\"flex min-w-0 flex-1 flex-col gap-2\">\n                      <div className=\"flex items-center gap-2\">\n                        <span className=\"flex items-center gap-2 font-medium text-sm\">\n                          {\" \"}\n                          <Avatar className=\"size-8\">\n                            <AvatarImage\n                              alt={member.name}\n                              src={member.avatar}\n                            />\n                            <AvatarFallback>\n                              {getInitials(member.name)}\n                            </AvatarFallback>\n                          </Avatar>\n                          {member.name}\n                        </span>\n                        {isCurrentUser && (\n                          <Badge className=\"text-xs\" variant=\"secondary\">\n                            You\n                          </Badge>\n                        )}\n                        <Badge\n                          className=\"text-xs\"\n                          variant={getRoleBadgeVariant(member.role)}\n                        >\n                          <RoleIcon className=\"mr-1 size-3\" />\n                          {member.role}\n                        </Badge>\n                        {member.status === \"invited\" && (\n                          <Badge className=\"text-xs\" variant=\"outline\">\n                            Invited\n                          </Badge>\n                        )}\n                        {member.status === \"suspended\" && (\n                          <Badge className=\"text-xs\" variant=\"destructive\">\n                            Suspended\n                          </Badge>\n                        )}\n                      </div>\n                      <p className=\"text-muted-foreground text-sm\">\n                        {member.email}\n                      </p>\n                      <div className=\"flex flex-wrap items-center gap-1 text-muted-foreground text-xs\">\n                        <span>Joined {formatDate(member.joinedAt)}</span>\n                        {member.lastActive && (\n                          <>\n                            <span aria-hidden=\"true\">•</span>\n                            <span>\n                              Active {formatLastActive(member.lastActive)}\n                            </span>\n                          </>\n                        )}\n                        {showUsage && member.aiUsage && (\n                          <>\n                            <span aria-hidden=\"true\">•</span>\n                            <span>\n                              {formatNumber(member.aiUsage.tokens)} tokens\n                            </span>\n                            <span aria-hidden=\"true\">•</span>\n                            <span>\n                              {member.aiUsage.sessions} session\n                              {member.aiUsage.sessions !== 1 ? \"s\" : \"\"}\n                            </span>\n                          </>\n                        )}\n                      </div>\n                    </div>\n                    {canModify && (\n                      <DropdownMenu>\n                        <DropdownMenuTrigger asChild>\n                          <Button\n                            aria-label={`More options for ${member.name}`}\n                            size=\"icon\"\n                            type=\"button\"\n                            variant=\"ghost\"\n                          >\n                            {actionLoading === member.id ? (\n                              <Loader2 className=\"size-4 animate-spin\" />\n                            ) : (\n                              <MoreVertical className=\"size-4\" />\n                            )}\n                          </Button>\n                        </DropdownMenuTrigger>\n                        <DropdownMenuContent\n                          align=\"end\"\n                          collisionPadding={8}\n                          sideOffset={4}\n                        >\n                          {member.role === \"member\" && onPromote && (\n                            <DropdownMenuItem\n                              onSelect={() =>\n                                handleAction(\n                                  () => onPromote(member.id),\n                                  member.id\n                                )\n                              }\n                            >\n                              <Shield className=\"size-4\" />\n                              Promote to Admin\n                            </DropdownMenuItem>\n                          )}\n                          {member.role === \"admin\" && onDemote && (\n                            <DropdownMenuItem\n                              onSelect={() =>\n                                handleAction(\n                                  () => onDemote(member.id),\n                                  member.id\n                                )\n                              }\n                            >\n                              <UserCheck className=\"size-4\" />\n                              Demote to Member\n                            </DropdownMenuItem>\n                          )}\n                          {member.status === \"invited\" && onResendInvite && (\n                            <DropdownMenuItem\n                              onSelect={() =>\n                                handleAction(\n                                  () => onResendInvite(member.id),\n                                  member.id\n                                )\n                              }\n                            >\n                              <Mail className=\"size-4\" />\n                              Resend Invitation\n                            </DropdownMenuItem>\n                          )}\n                          {member.role !== \"viewer\" && (\n                            <DropdownMenuSeparator />\n                          )}\n                          {onRemove && (\n                            <>\n                              <DropdownMenuItem\n                                onSelect={() =>\n                                  handleAction(\n                                    () => onRemove(member.id),\n                                    member.id\n                                  )\n                                }\n                                variant=\"destructive\"\n                              >\n                                <Trash2 className=\"size-4\" />\n                                Remove Member\n                              </DropdownMenuItem>\n                            </>\n                          )}\n                        </DropdownMenuContent>\n                      </DropdownMenu>\n                    )}\n                  </div>\n                  {idx < filteredMembers.length - 1 && <Separator />}\n                </div>\n              );\n            })}\n          </div>\n        )}\n      </CardContent>\n    </Card>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "block",
    "team"
  ],
  "type": "registry:ui"
}