{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "presence-stack",
  "type": "registry:component",
  "title": "Presence Stack",
  "description": "Overlapping live-presence avatars with status dots and a sane overflow chip.",
  "dependencies": [
    "@vllnt/ui@^0.2.1"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/presence-stack/presence-stack.tsx",
      "content": "\"use client\";\n\nimport {\n  type ComponentPropsWithoutRef,\n  forwardRef,\n  type ReactNode,\n} from \"react\";\n\nimport { cn } from \"@vllnt/ui\";\n\n/**\n * Presence status — drives the corner dot color.\n *\n * @public\n */\nexport type PresenceStatus = \"active\" | \"away\" | \"idle\" | \"offline\";\n\nconst STATUS_DOT: Record<PresenceStatus, string> = {\n  active: \"bg-emerald-500\",\n  away: \"bg-amber-500\",\n  idle: \"bg-muted-foreground\",\n  offline: \"bg-muted-foreground/40\",\n};\n\n/**\n * One user in the presence stack.\n *\n * @public\n */\nexport type PresenceUser = {\n  /** Optional accent color (hex / oklch / var). Drives the avatar fill. */\n  color?: string;\n  /** Stable identifier — used as the React key. */\n  id: string;\n  /** Avatar initial / glyph. */\n  initial: ReactNode;\n  /** Display name shown on hover (`title` attribute). */\n  name?: string;\n  /** Optional status. Defaults to `\"active\"`. */\n  status?: PresenceStatus;\n};\n\n/**\n * Localizable strings.\n *\n * @public\n */\nexport type PresenceStackLabels = {\n  /** Suffix for the overflow chip. Defaults to `\"more\"`. */\n  overflowSuffix?: string;\n  /** Aria-label override. Defaults to `\"Live presence\"`. */\n  region?: string;\n};\n\nconst DEFAULT_LABELS = {\n  overflowSuffix: \"more\",\n  region: \"Live presence\",\n} as const satisfies Required<PresenceStackLabels>;\n\n/**\n * Props for {@link PresenceStack}.\n *\n * @public\n */\nexport type PresenceStackProps = {\n  /** Localizable strings. */\n  labels?: PresenceStackLabels;\n  /** Cap the rendered users; the rest collapse into a `+N` chip. Defaults to `5`. */\n  max?: number;\n  /** Optional click handler for the overflow chip. */\n  onOverflowActivate?: () => void;\n  /** Users sorted in render order (most-relevant first). */\n  users: PresenceUser[];\n} & ComponentPropsWithoutRef<\"div\">;\n\nconst Avatar = (props: { user: PresenceUser }): React.ReactElement => {\n  const { user } = props;\n  const status = user.status ?? \"active\";\n  return (\n    <span\n      className=\"relative -ml-2 inline-flex size-7 items-center justify-center rounded-full border-2 border-background text-[11px] font-semibold text-white shadow-sm first:ml-0\"\n      data-presence-stack-status={status}\n      data-presence-stack-user={user.id}\n      style={{ backgroundColor: user.color ?? \"var(--foreground)\" }}\n      title={user.name}\n    >\n      {user.initial}\n      <span\n        aria-hidden=\"true\"\n        className={cn(\n          \"absolute -bottom-0.5 -right-0.5 size-2 rounded-full border border-background\",\n          STATUS_DOT[status],\n        )}\n        data-presence-stack-dot\n      />\n    </span>\n  );\n};\n\n/**\n * Overlapping live-presence avatars showing who is in the canvas right\n * now. Each avatar carries an accent color, an initial, and a status\n * dot. Distinct from `AvatarGroup` (a static participant grouping):\n * this primitive centers on live status, a sane overflow, and\n * interactive expansion.\n *\n * Pure presentation; the host owns the websocket roster + maps user\n * ids to colors.\n *\n * @example\n * ```tsx\n * <PresenceStack\n *   max={4}\n *   users={[\n *     { id: \"1\", initial: \"B\", color: \"#5b8def\", name: \"Bea\" },\n *     { id: \"2\", initial: \"L\", color: \"#10b981\", name: \"Lior\", status: \"away\" },\n *     { id: \"3\", initial: \"S\", color: \"#f59e0b\", name: \"Sam\", status: \"idle\" },\n *   ]}\n *   onOverflowActivate={() => openRoster()}\n * />\n * ```\n *\n * @public\n */\nexport const PresenceStack = forwardRef<HTMLDivElement, PresenceStackProps>(\n  (props, ref) => {\n    const {\n      className,\n      labels,\n      max = 5,\n      onOverflowActivate,\n      users,\n      ...rest\n    } = props;\n    const resolvedLabels = { ...DEFAULT_LABELS, ...labels };\n    const visible = max >= users.length ? users : users.slice(0, max);\n    const hidden = users.length - visible.length;\n    const handleOverflow = (): void => {\n      onOverflowActivate?.();\n    };\n    return (\n      <div\n        aria-label={resolvedLabels.region}\n        className={cn(\"inline-flex items-center pl-2\", className)}\n        data-presence-stack\n        ref={ref}\n        role=\"group\"\n        {...rest}\n      >\n        {visible.map((user) => (\n          <Avatar key={user.id} user={user} />\n        ))}\n        {hidden > 0\n          ? renderOverflow({\n              count: hidden,\n              handleClick: handleOverflow,\n              handlerProvided: Boolean(onOverflowActivate),\n              labels: resolvedLabels,\n            })\n          : null}\n      </div>\n    );\n  },\n);\nPresenceStack.displayName = \"PresenceStack\";\n\nconst renderOverflow = (input: {\n  count: number;\n  handleClick: () => void;\n  handlerProvided: boolean;\n  labels: Required<PresenceStackLabels>;\n}): React.ReactElement => {\n  const text = `+${input.count}`;\n  const aria = `${input.count} ${input.labels.overflowSuffix}`;\n  const className =\n    \"relative -ml-2 inline-flex h-7 min-w-7 items-center justify-center rounded-full border-2 border-background bg-muted px-1.5 text-[10px] font-semibold text-muted-foreground shadow-sm\";\n  if (input.handlerProvided) {\n    return (\n      <button\n        aria-label={aria}\n        className={cn(\n          className,\n          \"transition-colors hover:bg-muted/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n        )}\n        data-presence-stack-overflow\n        onClick={input.handleClick}\n        type=\"button\"\n      >\n        {text}\n      </button>\n    );\n  }\n  return (\n    <span aria-label={aria} className={className} data-presence-stack-overflow>\n      {text}\n    </span>\n  );\n};\n",
      "type": "registry:component"
    }
  ],
  "version": "0.2.1",
  "stability": "stable"
}
