{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "collaboration",
  "title": "Collaboration",
  "description": "A user collaboration blok with nested popovers for adding and removing team members with search functionality.",
  "dependencies": [
    "@mdi/js"
  ],
  "registryDependencies": [
    "https://blok.sitecore.com/r/theme.json",
    "https://blok.sitecore.com/r/avatar.json",
    "https://blok.sitecore.com/r/button.json",
    "https://blok.sitecore.com/r/card.json",
    "https://blok.sitecore.com/r/search-input.json",
    "https://blok.sitecore.com/r/popover.json",
    "https://blok.sitecore.com/r/icon.json"
  ],
  "files": [
    {
      "path": "src/components/bloks/collaboration.tsx",
      "content": "\"use client\"; // Remove this line if you are not using Next.js\n\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card } from \"@/components/ui/card\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport {\n  SearchInput,\n  SearchInputClearButton,\n  SearchInputField,\n  SearchInputLeftElement,\n  SearchInputRightElement,\n} from \"@/components/ui/search-input\";\nimport { Icon } from \"@/lib/icon\";\nimport { mdiAccountPlusOutline, mdiClose, mdiMagnify, mdiPlus } from \"@mdi/js\";\nimport type * as React from \"react\";\nimport { useMemo, useState } from \"react\";\n\n// Types\n\n/** Base user interface - extend this for custom user types */\nexport interface User {\n  id: string;\n  name: string;\n  avatarUrl?: string;\n  email?: string;\n  [key: string]: unknown; // Allow additional properties\n}\n\nexport interface CollaborationProps<T extends User = User> {\n  /** Currently added users */\n  users: T[];\n  /** Current user (shown with \"You\" label) */\n  currentUser?: T;\n  /** Available users to add (shown in dialog) - can be managed externally for API support */\n  availableUsers: T[];\n  /** Maximum number of avatars to display in the stack */\n  maxDisplayAvatars?: number;\n  /** Callback when a user is added */\n  onAddUser?: (user: T) => void;\n  /** Callback when a user is removed */\n  onRemoveUser?: (userId: string) => void;\n  /** Allow removing the current user (default: false) */\n  allowRemoveCurrentUser?: boolean;\n  /** Custom empty state message */\n  emptyStateMessage?: string;\n  /** Title for the users section */\n  title?: string;\n  /** Custom class name */\n  className?: string;\n\n  // API & Customization Props\n\n  /** Callback when search query changes - use for API-based search */\n  onSearch?: (query: string) => void;\n  /** Shows loading spinner in search results */\n  isSearching?: boolean;\n  /** Custom search placeholder text */\n  searchPlaceholder?: string;\n  /** Custom function to get user's display name */\n  getDisplayName?: (user: T) => string;\n  /** Custom function to get user's avatar URL */\n  getAvatarUrl?: (user: T) => string | undefined;\n  /** Custom function to get user's unique ID */\n  getUserId?: (user: T) => string;\n  /** Disable built-in client-side filtering (use when doing API search) */\n  disableClientFilter?: boolean;\n  /** Custom empty search results message */\n  emptySearchMessage?: string;\n  /** Custom \"all users added\" message */\n  allUsersAddedMessage?: string;\n  /** Custom \"no users available\" message */\n  noUsersAvailableMessage?: string;\n}\n\nfunction getInitials(name: string): string {\n  return name\n    .split(\" \")\n    .map((part) => part.charAt(0))\n    .join(\"\")\n    .toUpperCase()\n    .slice(0, 2);\n}\n\n/** Avatar trigger that opens the popover - shows user avatars + add button */\nfunction AvatarTrigger({\n  users,\n  maxDisplay = 3,\n}: {\n  users: User[];\n  maxDisplay?: number;\n}) {\n  const displayUsers = users.slice(0, maxDisplay);\n  const remainingCount = Math.max(0, users.length - maxDisplay);\n  const hasOverflow = remainingCount > 0;\n\n  return (\n    <div className=\"flex items-center -space-x-2 cursor-pointer\">\n      {users.length > 0 &&\n        displayUsers.map((user) => (\n          <Avatar key={user.id} className=\"size-8 ring-0 border-2 border-white\">\n            <AvatarImage src={user.avatarUrl} alt={user.name} />\n            <AvatarFallback className=\"text-xs bg-muted\">\n              {getInitials(user.name)}\n            </AvatarFallback>\n          </Avatar>\n        ))}\n      {/* Show +n count when overflow, otherwise show add user icon */}\n      {hasOverflow ? (\n        <div className=\"flex size-8 items-center justify-center rounded-full bg-primary-background text-primary-foreground border-2 border-white transition-colors hover:bg-primary hover:text-white\">\n          <span className=\"text-xs font-medium\">+{remainingCount}</span>\n        </div>\n      ) : (\n        <div className=\"flex size-8 items-center justify-center rounded-full bg-primary-background text-primary-foreground transition-colors hover:bg-primary hover:text-white border-2 border-white\">\n          <Icon path={mdiAccountPlusOutline} className=\"size-4\" />\n        </div>\n      )}\n    </div>\n  );\n}\n\nfunction UserListItem({\n  user,\n  isCurrentUser = false,\n  isAdded = false,\n  onAdd,\n  onRemove,\n}: {\n  user: User;\n  isCurrentUser?: boolean;\n  isAdded?: boolean;\n  onAdd?: () => void;\n  onRemove?: () => void;\n}) {\n  return (\n    <div className=\"group flex min-w-0 items-center justify-between gap-2 py-2 px-1 rounded-md hover:bg-neutral-bg transition-colors\">\n      <div className=\"flex min-w-0 flex-1 items-center gap-3\">\n        <Avatar className=\"size-8\">\n          <AvatarImage src={user.avatarUrl} alt={user.name} />\n          <AvatarFallback className=\"text-xs bg-muted\">\n            {getInitials(user.name)}\n          </AvatarFallback>\n        </Avatar>\n        <span className=\"truncate text-sm font-medium\">\n          {user.name}\n          {isCurrentUser && (\n            <span className=\"text-muted-foreground\"> (You)</span>\n          )}\n        </span>\n      </div>\n      {isAdded && onRemove && (\n        <Button\n          variant=\"link\"\n          size=\"xs\"\n          colorScheme=\"primary\"\n          onClick={onRemove}\n          className=\"h-auto py-0.5 px-2 no-underline hover:no-underline font-bold opacity-0 group-hover:opacity-100 focus-visible:opacity-100\"\n        >\n          Remove\n        </Button>\n      )}\n      {!isAdded && onAdd && (\n        <Button\n          variant=\"ghost\"\n          size=\"icon-xs\"\n          onClick={onAdd}\n          className=\"text-muted-foreground hover:text-foreground\"\n        >\n          <Icon path={mdiPlus} className=\"size-4\" />\n        </Button>\n      )}\n    </div>\n  );\n}\n\n// Main Component\n\nexport function Collaboration<T extends User = User>({\n  users,\n  currentUser,\n  availableUsers,\n  maxDisplayAvatars = 3,\n  onAddUser,\n  onRemoveUser,\n  allowRemoveCurrentUser = false,\n  emptyStateMessage = \"No users yet\",\n  title = \"Users\",\n  className,\n  // API & Customization Props\n  onSearch,\n  isSearching = false,\n  searchPlaceholder = \"Search\",\n  getDisplayName = (user: T) => user.name,\n  // eslint-disable-next-line @typescript-eslint/no-unused-vars\n  getAvatarUrl: _getAvatarUrl = (user: T) => user.avatarUrl,\n  getUserId = (user: T) => user.id,\n  disableClientFilter = false,\n  emptySearchMessage,\n  allUsersAddedMessage = \"All available users have been added\",\n  noUsersAvailableMessage = \"No users available to add\",\n  ...props\n}: CollaborationProps<T> & React.ComponentProps<\"div\">) {\n  const [isUsersOpen, setIsUsersOpen] = useState(false);\n  const [isAddUsersOpen, setIsAddUsersOpen] = useState(false);\n  const [searchQuery, setSearchQuery] = useState(\"\");\n\n  // Filter out already added users from available users\n  const addedUserIds = useMemo(\n    () => new Set(users.map((u) => getUserId(u as T))),\n    [users, getUserId],\n  );\n\n  const filteredAvailableUsers = useMemo(() => {\n    let filtered = availableUsers.filter(\n      (u) => !addedUserIds.has(getUserId(u)),\n    );\n\n    if (!disableClientFilter && searchQuery) {\n      filtered = filtered.filter((u) => {\n        const name = getDisplayName(u).toLowerCase();\n        const email = (u.email || \"\").toLowerCase();\n        const query = searchQuery.toLowerCase();\n        return name.includes(query) || email.includes(query);\n      });\n    }\n\n    return filtered;\n  }, [\n    availableUsers,\n    addedUserIds,\n    searchQuery,\n    disableClientFilter,\n    getDisplayName,\n    getUserId,\n  ]);\n\n  const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    const query = e.target.value;\n    setSearchQuery(query);\n    onSearch?.(query);\n  };\n\n  const handleAddUser = (user: T) => {\n    onAddUser?.(user);\n  };\n\n  const handleRemoveUser = (userId: string) => {\n    onRemoveUser?.(userId);\n  };\n\n  const handleUsersOpenChange = (open: boolean) => {\n    setIsUsersOpen(open);\n    if (!open) {\n      // Close add users popover when users popover closes\n      setIsAddUsersOpen(false);\n      setSearchQuery(\"\");\n    }\n  };\n\n  const handleAddUsersOpenChange = (open: boolean) => {\n    setIsAddUsersOpen(open);\n    if (!open) {\n      setSearchQuery(\"\");\n    }\n  };\n\n  const userCount = users.length;\n  const isEmpty = userCount === 0;\n\n  return (\n    <div className={`w-fit ${className ?? \"\"}`} {...props}>\n      {/* First Popover: Avatar -> Users Card */}\n      <Popover open={isUsersOpen} onOpenChange={handleUsersOpenChange}>\n        <PopoverTrigger asChild>\n          <Button\n            type=\"button\"\n            variant=\"ghost\"\n            className=\"h-auto min-h-0 w-fit rounded-full p-0 bg-transparent hover:bg-transparent active:bg-transparent focus-visible:ring-2 focus-visible:ring-primary\"\n          >\n            <AvatarTrigger users={users} maxDisplay={maxDisplayAvatars} />\n          </Button>\n        </PopoverTrigger>\n\n        <PopoverContent\n          align=\"end\"\n          sideOffset={8}\n          aria-label={title}\n          className=\"w-[368px] p-0 border-0 bg-transparent shadow-none\"\n        >\n          {/* Users Panel */}\n          <Card style=\"outline\" padding=\"md\" elevation=\"md\" className=\"gap-0\">\n            <div className=\"pb-2\">\n              <div className=\"text-[18px] font-semibold leading-[1.2] flex items-baseline gap-1\">\n                <span>{title}</span>\n                <span className=\"text-muted-foreground text-sm\">\n                  {userCount}\n                </span>\n              </div>\n            </div>\n\n            <div className=\"pb-2\">\n              {isEmpty ? (\n                <div className=\"text-sm text-muted-foreground py-2\">\n                  {emptyStateMessage}\n                </div>\n              ) : (\n                <div className=\"max-h-48 space-y-1 overflow-y-auto overflow-x-hidden\">\n                  {users.map((user) => {\n                    const userId = getUserId(user as T);\n                    const currentUserId = currentUser\n                      ? getUserId(currentUser as T)\n                      : undefined;\n                    const isCurrentUserItem = currentUserId === userId;\n                    const canRemove =\n                      !isCurrentUserItem || allowRemoveCurrentUser;\n                    return (\n                      <UserListItem\n                        key={userId}\n                        user={user}\n                        isCurrentUser={isCurrentUserItem}\n                        isAdded={true}\n                        onRemove={\n                          canRemove ? () => handleRemoveUser(userId) : undefined\n                        }\n                      />\n                    );\n                  })}\n                </div>\n              )}\n\n              {/* Second Popover: Add users button -> Add Users Card */}\n              <Popover\n                open={isAddUsersOpen}\n                onOpenChange={handleAddUsersOpenChange}\n              >\n                <PopoverTrigger asChild>\n                  <Button\n                    variant=\"ghost\"\n                    size=\"default\"\n                    className=\"w-full justify-start gap-2 text-muted-foreground hover:text-foreground mt-2 px-1\"\n                  >\n                    <Icon path={mdiAccountPlusOutline} className=\"size-4\" />\n                    <span className=\"font-semibold leading-[1.2]\">\n                      Add users\n                    </span>\n                  </Button>\n                </PopoverTrigger>\n\n                <PopoverContent\n                  side=\"bottom\"\n                  align=\"center\"\n                  sideOffset={-8}\n                  alignOffset={26}\n                  aria-label=\"Add users\"\n                  className=\"w-[420px] p-0 border-0 bg-transparent shadow-none\"\n                >\n                  {/* Add Users Panel */}\n                  <Card\n                    style=\"outline\"\n                    padding=\"md\"\n                    elevation=\"md\"\n                    className=\"gap-0\"\n                  >\n                    <div className=\"pb-2\">\n                      <div className=\"flex items-center justify-between\">\n                        <div className=\"text-[18px] font-semibold leading-[1.2]\">\n                          Add users\n                        </div>\n                        <Button\n                          variant=\"ghost\"\n                          size=\"icon-sm\"\n                          onClick={() => setIsAddUsersOpen(false)}\n                          className=\"text-muted-foreground hover:text-foreground -mr-1\"\n                          aria-label=\"Close add users popover\"\n                        >\n                          <Icon path={mdiClose} className=\"size-4\" />\n                        </Button>\n                      </div>\n                    </div>\n\n                    <div className=\"pb-2\">\n                      <div className=\"mb-3\">\n                        <SearchInput>\n                          <SearchInputLeftElement>\n                            <Icon path={mdiMagnify} size={0.9} />\n                          </SearchInputLeftElement>\n                          <SearchInputField\n                            placeholder={searchPlaceholder}\n                            value={searchQuery}\n                            onChange={handleSearchChange}\n                            autoFocus\n                          />\n                          {searchQuery && (\n                            <SearchInputRightElement>\n                              <SearchInputClearButton\n                                onClear={() => {\n                                  setSearchQuery(\"\");\n                                  onSearch?.(\"\");\n                                }}\n                                tooltipLabel=\"Clear search\"\n                              />\n                            </SearchInputRightElement>\n                          )}\n                        </SearchInput>\n                      </div>\n\n                      <div className=\"max-h-48 space-y-1 overflow-y-auto overflow-x-hidden\">\n                        {isSearching && (\n                          <div className=\"text-sm text-muted-foreground py-4 text-center\">\n                            Searching...\n                          </div>\n                        )}\n\n                        {!isSearching &&\n                          filteredAvailableUsers.map((user) => (\n                            <UserListItem\n                              key={getUserId(user)}\n                              user={user}\n                              isAdded={false}\n                              onAdd={() => handleAddUser(user)}\n                            />\n                          ))}\n\n                        {!isSearching &&\n                          filteredAvailableUsers.length === 0 &&\n                          searchQuery && (\n                            <div className=\"text-sm text-muted-foreground py-4 text-center\">\n                              {emptySearchMessage ||\n                                `No users found matching \"${searchQuery}\"`}\n                            </div>\n                          )}\n                        {!isSearching &&\n                          filteredAvailableUsers.length === 0 &&\n                          !searchQuery &&\n                          availableUsers.length > 0 && (\n                            <div className=\"text-sm text-muted-foreground py-4 text-center\">\n                              {allUsersAddedMessage}\n                            </div>\n                          )}\n                        {!isSearching && availableUsers.length === 0 && (\n                          <div className=\"text-sm text-muted-foreground py-4 text-center\">\n                            {noUsersAvailableMessage}\n                          </div>\n                        )}\n                      </div>\n                    </div>\n                  </Card>\n                </PopoverContent>\n              </Popover>\n            </div>\n          </Card>\n        </PopoverContent>\n      </Popover>\n    </div>\n  );\n}\n\nexport default Collaboration;\n",
      "type": "registry:block",
      "target": "src/components/bloks/collaboration.tsx"
    }
  ],
  "type": "registry:block"
}