{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "workspace-card",
  "title": "Workspace Card",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A workspace or team card with a generated cover and logo tile, plan, member faces, project and storage stats, a current badge, an actions menu and a loading state.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/avatar.json",
    "https://ui.ballmac.com/r/dropdown-menu.json",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/workspace-card.tsx",
      "content": "// Ballmac UI: Workspace Card. https://ui.ballmac.com/components/workspace-card\n\"use client\";\n\nimport * as React from \"react\";\nimport { ArrowUpRight, Check, FolderKanban, MoreHorizontal } from \"lucide-react\";\nimport { Avatar, AvatarFallback, AvatarGroup, AvatarImage } from \"@/components/ballmac/avatar\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@/components/ballmac/dropdown-menu\";\nimport { cn } from \"@/lib/utils\";\nimport { useMessages } from \"@/lib/ballmac/i18n\";\n\ntype WorkspaceMember = {\n  name: string;\n  image?: string;\n};\n\ntype WorkspaceAction = {\n  label: string;\n  onSelect: () => void;\n  /** Style as a destructive action, for example \"Leave workspace\". */\n  destructive?: boolean;\n};\n\ntype Workspace = {\n  name: string;\n  /** One line about the workspace. */\n  description?: string;\n  /** Plan label, for example \"Team\". */\n  plan?: string;\n  /** A few members to show as faces. */\n  members?: WorkspaceMember[];\n  /** Total members, when more than `members` lists. */\n  memberCount?: number;\n  /** Number of projects. */\n  projects?: number;\n  /** Storage in use and included, with a unit. */\n  storage?: { used: number; total: number; unit: string };\n  /** Text such as \"Active 2 hours ago\". */\n  lastActive?: string;\n};\n\ntype WorkspaceCardProps = Omit<React.ComponentProps<\"article\">, \"children\"> & {\n  /** The workspace to show. Optional only while `loading`. */\n  workspace?: Workspace;\n  /** Where the card goes when opened. Makes the whole card clickable through its title. */\n  href?: string;\n  /** Called when the title button is pressed (when no `href`). */\n  onOpen?: () => void;\n  /** Mark as the workspace you are in now. */\n  current?: boolean;\n  /** Entries for the \"more\" menu. Leave empty to hide the menu. */\n  actions?: WorkspaceAction[];\n  /** Show a placeholder while data loads. */\n  loading?: boolean;\n};\n\nconst COVERS = [\n  \"from-chart-1/40 via-chart-4/25 to-chart-5/20\",\n  \"from-chart-2/40 via-chart-1/25 to-chart-4/20\",\n  \"from-chart-3/40 via-chart-5/25 to-chart-2/20\",\n  \"from-chart-4/40 via-chart-2/25 to-chart-3/20\",\n  \"from-chart-5/40 via-chart-3/25 to-chart-1/20\",\n];\nconst TILES = [\"bg-chart-1\", \"bg-chart-2\", \"bg-chart-3\", \"bg-chart-4\", \"bg-chart-5\"];\n\nfunction hash(text: string) {\n  let h = 0;\n  for (let i = 0; i < text.length; i++) h = (h * 31 + text.charCodeAt(i)) >>> 0;\n  return h;\n}\n\n/**\n * A card for one workspace or team: a generated cover and logo tile, plan, faces of members, project and storage\n * stats, and a menu of actions. The whole card is clickable through its title link, so the keyboard target is one clear link.\n */\nfunction WorkspaceCard({\n  workspace: workspaceProp,\n  href,\n  onOpen,\n  current = false,\n  actions = [],\n  loading = false,\n  className,\n  ...props\n}: WorkspaceCardProps) {\n  const msg = useMessages()\n  if (loading) {\n    return (\n      <article\n        data-slot=\"workspace-card\"\n        aria-busy=\"true\"\n        aria-label={msg(\"workspace-card.loadingWorkspace\", \"Loading workspace\")}\n        className={cn(\"overflow-hidden rounded-2xl border bg-card\", className)}\n        {...props}\n      >\n        <div className=\"h-20 animate-pulse bg-muted motion-reduce:animate-none\" />\n        <div className=\"grid gap-3 p-5 pt-8\">\n          <div className=\"h-4 w-1/2 animate-pulse rounded bg-muted motion-reduce:animate-none\" />\n          <div className=\"h-3 w-3/4 animate-pulse rounded bg-muted motion-reduce:animate-none\" />\n          <div className=\"h-8 w-full animate-pulse rounded bg-muted motion-reduce:animate-none\" />\n        </div>\n      </article>\n    );\n  }\n\n  const workspace = workspaceProp ?? { name: \"Workspace\" };\n  const h = hash(workspace.name);\n  const cover = COVERS[h % COVERS.length];\n  const tile = TILES[(h >> 3) % TILES.length];\n  const storagePercent = workspace.storage ? Math.min(100, (workspace.storage.used / Math.max(workspace.storage.total, 1)) * 100) : 0;\n  const total = workspace.memberCount ?? workspace.members?.length ?? 0;\n  const titleClass =\n    \"rounded-sm text-base font-semibold tracking-tight outline-none after:absolute after:inset-0 after:content-[''] focus-visible:after:ring-[3px] focus-visible:after:ring-inset focus-visible:after:ring-ring/50 focus-visible:after:rounded-2xl\";\n\n  return (\n    <article\n      data-slot=\"workspace-card\"\n      data-current={current || undefined}\n      className={cn(\n        \"group/ws relative overflow-hidden rounded-2xl border bg-card text-card-foreground shadow-[0_1px_2px_rgb(0_0_0/0.04)] transition-[box-shadow,transform,border-color] duration-200 hover:-translate-y-0.5 hover:shadow-[0_16px_40px_-20px_rgb(0_0_0/0.3)] motion-reduce:transition-none motion-reduce:hover:translate-y-0\",\n        current && \"border-primary/60\",\n        className,\n      )}\n      {...props}\n    >\n      <div aria-hidden=\"true\" className={cn(\"relative h-20 bg-gradient-to-br\", cover)}>\n        <div className=\"absolute inset-0 bg-[radial-gradient(circle_at_20%_0%,rgb(255_255_255/0.35),transparent_55%)] dark:bg-[radial-gradient(circle_at_20%_0%,rgb(255_255_255/0.08),transparent_55%)]\" />\n      </div>\n      {actions.length > 0 && (\n        <div className=\"absolute top-2.5 end-2.5 z-10\">\n          <DropdownMenu>\n            <DropdownMenuTrigger\n              aria-label={msg(\"workspace-card.actions\", \"{name} actions\", { name: workspace.name })}\n              className=\"inline-flex size-8 items-center justify-center rounded-full bg-background/70 text-foreground backdrop-blur outline-none transition-colors hover:bg-background focus-visible:ring-[3px] focus-visible:ring-ring/50 data-[state=open]:bg-background\"\n            >\n              <MoreHorizontal aria-hidden=\"true\" className=\"size-4\" />\n            </DropdownMenuTrigger>\n            <DropdownMenuContent align=\"end\" className=\"w-48\">\n              {actions.map((a, i) => (\n                <React.Fragment key={a.label}>\n                  {a.destructive && i > 0 && <DropdownMenuSeparator />}\n                  <DropdownMenuItem destructive={a.destructive} onSelect={a.onSelect}>\n                    {a.label}\n                  </DropdownMenuItem>\n                </React.Fragment>\n              ))}\n            </DropdownMenuContent>\n          </DropdownMenu>\n        </div>\n      )}\n\n      <div className=\"relative px-5 pb-5\">\n        <span\n          aria-hidden=\"true\"\n          className={cn(\n            \"-mt-7 flex size-14 items-center justify-center rounded-2xl text-xl font-semibold text-background shadow-[0_6px_16px_-4px_rgb(0_0_0/0.3)] ring-4 ring-card\",\n            tile,\n          )}\n        >\n          {workspace.name.charAt(0).toUpperCase()}\n        </span>\n\n        <div className=\"mt-3 flex items-start justify-between gap-3\">\n          <div className=\"min-w-0\">\n            <h3 className=\"flex items-center gap-2\">\n              {href ? (\n                <a href={href} className={titleClass}>\n                  {workspace.name}\n                </a>\n              ) : (\n                <button type=\"button\" onClick={onOpen} className={cn(titleClass, \"text-start\")}>\n                  {workspace.name}\n                </button>\n              )}\n              {current && (\n                <span className=\"relative inline-flex items-center gap-1 rounded-full bg-primary px-2 py-0.5 text-[10px] font-semibold tracking-wide text-primary-foreground uppercase\">\n                  <Check aria-hidden=\"true\" className=\"size-3\" strokeWidth={3} /> {msg(\"workspace-card.current\", \"Current\")}\n                </span>\n              )}\n            </h3>\n            {workspace.description && <p className=\"mt-1 line-clamp-2 text-sm leading-snug text-muted-foreground\">{workspace.description}</p>}\n          </div>\n          <ArrowUpRight\n            aria-hidden=\"true\"\n            className=\"mt-1 size-4 shrink-0 text-muted-foreground opacity-0 transition-[opacity,transform] group-hover/ws:translate-x-0.5 group-hover/ws:-translate-y-0.5 group-hover/ws:opacity-100 motion-reduce:transition-none rtl:-scale-x-100\"\n          />\n        </div>\n\n        <div className=\"mt-4 flex items-center justify-between gap-3\">\n          {workspace.members && workspace.members.length > 0 ? (\n            <AvatarGroup size=\"sm\" max={4} total={total}>\n              {workspace.members.map((m) => (\n                <Avatar key={m.name}>\n                  {m.image && <AvatarImage src={m.image} alt={m.name} />}\n                  <AvatarFallback>{m.name.charAt(0).toUpperCase()}</AvatarFallback>\n                </Avatar>\n              ))}\n            </AvatarGroup>\n          ) : (\n            <span className=\"text-xs text-muted-foreground\">{msg(\"workspace-card.members\", { one: \"{count} member\", other: \"{count} members\" }, { count: total })}</span>\n          )}\n          {workspace.plan && (\n            <span className=\"rounded-full border bg-muted/60 px-2.5 py-0.5 text-xs font-medium\">{workspace.plan}</span>\n          )}\n        </div>\n\n        {(workspace.projects !== undefined || workspace.storage) && (\n          <dl className=\"mt-4 grid grid-cols-2 gap-3 border-t pt-4 text-sm\">\n            {workspace.projects !== undefined && (\n              <div>\n                <dt className=\"text-xs text-muted-foreground\">Projects</dt>\n                <dd className=\"mt-0.5 flex items-center gap-1.5 font-medium tabular-nums\">\n                  <FolderKanban aria-hidden=\"true\" className=\"size-3.5 text-muted-foreground\" />\n                  {workspace.projects}\n                </dd>\n              </div>\n            )}\n            {workspace.storage && (\n              <div>\n                <dt className=\"text-xs text-muted-foreground\">Storage</dt>\n                <dd className=\"mt-0.5 grid gap-1.5\">\n                  <span className=\"font-medium tabular-nums\">\n                    {workspace.storage.used} <span className=\"text-xs font-normal text-muted-foreground\">/ {workspace.storage.total} {workspace.storage.unit}</span>\n                  </span>\n                  <span\n                    role=\"meter\"\n                    aria-label={msg(\"workspace-card.storageUsed\", \"Storage used\")}\n                    aria-valuemin={0}\n                    aria-valuemax={workspace.storage.total}\n                    aria-valuenow={workspace.storage.used}\n                    className=\"h-1 overflow-hidden rounded-full bg-muted\"\n                  >\n                    <span className={cn(\"block h-full rounded-full\", storagePercent >= 90 ? \"bg-destructive\" : storagePercent >= 75 ? \"bg-chart-3\" : \"bg-primary\")} style={{ width: `${storagePercent}%` }} />\n                  </span>\n                </dd>\n              </div>\n            )}\n          </dl>\n        )}\n        {workspace.lastActive && <p className=\"mt-3 text-xs text-muted-foreground\">{workspace.lastActive}</p>}\n      </div>\n    </article>\n  );\n}\n\nexport { WorkspaceCard, type WorkspaceCardProps, type Workspace, type WorkspaceMember, type WorkspaceAction };\n",
      "type": "registry:ui",
      "target": "@components/ballmac/workspace-card.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "workspace",
      "team",
      "card",
      "members"
    ],
    "version": "1.0.0",
    "updated": "2026-09-30",
    "ai": {
      "summary": "workspace {name,description,plan,members,memberCount,projects,storage,lastActive}, href or onOpen, current, actions[], loading. The title is the card's single link.",
      "whenToUse": [
        "Workspace pickers and team lists",
        "Account overview pages"
      ],
      "whenNotToUse": [
        "A dropdown switcher; use team-switcher",
        "Generic content cards; use card"
      ],
      "composesWith": [
        "team-switcher",
        "avatar",
        "usage-meter"
      ],
      "a11y": [
        {
          "keys": "Tab",
          "action": "One link per card (the title); the actions menu is a separate button"
        },
        {
          "keys": "Screen readers",
          "action": "Storage is a labelled meter; loading state sets aria-busy"
        }
      ],
      "customization": [
        "current",
        "actions",
        "href vs onOpen",
        "cover and logo color come from the name"
      ]
    },
    "examples": [
      "workspace-card-demo",
      "workspace-card-states"
    ],
    "url": "https://ui.ballmac.com/components/workspace-card"
  },
  "categories": [
    "saas"
  ],
  "type": "registry:ui"
}