{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "thread-bubble",
  "type": "registry:component",
  "title": "Thread Bubble",
  "description": "Expanded discussion bubble for an anchored canvas comment thread.",
  "dependencies": [
    "@vllnt/ui@^0.2.1"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/thread-bubble/thread-bubble.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 * One message in a thread bubble.\n *\n * @public\n */\nexport type ThreadMessage = {\n  /** Author display name. */\n  author: ReactNode;\n  /** Optional accent color for the author chip. */\n  authorColor?: string;\n  /** Message body — rendered as-is, can be plain text or a `ReactNode`. */\n  body: ReactNode;\n  /** Stable identifier — used as the React key + analytics hook. */\n  id: string;\n  /** Pre-formatted timestamp (host owns formatting). */\n  ts?: ReactNode;\n};\n\n/**\n * Localizable strings.\n *\n * @public\n */\nexport type ThreadBubbleLabels = {\n  /** Empty-state copy. Defaults to `\"No replies yet\"`. */\n  empty?: string;\n  /** Aria-label override. Defaults to `\"Comment thread\"`. */\n  region?: string;\n};\n\nconst DEFAULT_LABELS = {\n  empty: \"No replies yet\",\n  region: \"Comment thread\",\n} as const satisfies Required<ThreadBubbleLabels>;\n\n/**\n * Props for {@link ThreadBubble}.\n *\n * @public\n */\nexport type ThreadBubbleProps = {\n  /** Optional footer slot — typically a reply input. */\n  footer?: ReactNode;\n  /** Localizable strings. */\n  labels?: ThreadBubbleLabels;\n  /** Messages newest-last. */\n  messages: ThreadMessage[];\n  /** Optional resolve handler. When provided, a \"Resolve\" button appears in the header. */\n  onResolve?: () => void;\n  /** Optional thread title (e.g. anchored object name). */\n  title?: ReactNode;\n} & ComponentPropsWithoutRef<\"section\">;\n\nconst Message = (props: { message: ThreadMessage }): React.ReactElement => {\n  const { message } = props;\n  return (\n    <li className=\"space-y-0.5\" data-thread-bubble-message={message.id}>\n      <header className=\"flex items-baseline justify-between gap-2 text-[10px]\">\n        <span\n          className=\"font-semibold\"\n          data-thread-bubble-author\n          style={\n            message.authorColor ? { color: message.authorColor } : undefined\n          }\n        >\n          {message.author}\n        </span>\n        {message.ts ? (\n          <span className=\"text-muted-foreground\" data-thread-bubble-ts>\n            {message.ts}\n          </span>\n        ) : null}\n      </header>\n      <p className=\"text-xs text-foreground\" data-thread-bubble-body>\n        {message.body}\n      </p>\n    </li>\n  );\n};\n\n/**\n * Expanded discussion bubble for an anchored canvas comment thread.\n * Renders a stacked message list plus an optional reply slot via\n * `footer`. Pair with {@link \"../comment-pin/comment-pin\".CommentPin}: pin marks the location,\n * bubble holds the conversation.\n *\n * Pure presentation; the host owns the message store + supplies the\n * resolve handler for hosts that allow threading.\n *\n * @example\n * ```tsx\n * <ThreadBubble\n *   title=\"research-2025\"\n *   messages={[\n *     { id: \"1\", author: \"Bea\", authorColor: \"#5b8def\", body: \"Why fallback?\", ts: \"12s\" },\n *     { id: \"2\", author: \"Lior\", authorColor: \"#10b981\", body: \"p95 spike — see graph\", ts: \"9s\" },\n *   ]}\n *   footer={<ReplyInput onSubmit={post} />}\n *   onResolve={resolve}\n * />\n * ```\n *\n * @public\n */\nexport const ThreadBubble = forwardRef<HTMLElement, ThreadBubbleProps>(\n  (props, ref) => {\n    const { className, footer, labels, messages, onResolve, title, ...rest } =\n      props;\n    const resolvedLabels = { ...DEFAULT_LABELS, ...labels };\n    const handleResolve = (): void => {\n      onResolve?.();\n    };\n    return (\n      <section\n        aria-label={resolvedLabels.region}\n        className={cn(\n          \"flex w-72 flex-col gap-2 rounded-lg border border-border bg-background p-3 text-foreground shadow-md\",\n          className,\n        )}\n        data-thread-bubble\n        ref={ref}\n        {...rest}\n      >\n        {title || onResolve ? (\n          <header className=\"flex items-center justify-between gap-2 text-[11px] uppercase tracking-wide text-muted-foreground\">\n            {title ? (\n              <span className=\"truncate font-semibold\" data-thread-bubble-title>\n                {title}\n              </span>\n            ) : (\n              <span aria-hidden=\"true\" />\n            )}\n            {onResolve ? (\n              <button\n                className=\"rounded-full border border-border px-2 py-0.5 text-[10px] font-medium text-muted-foreground transition-colors hover:bg-muted/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n                data-thread-bubble-resolve\n                onClick={handleResolve}\n                type=\"button\"\n              >\n                Resolve\n              </button>\n            ) : null}\n          </header>\n        ) : null}\n        {messages.length === 0 ? (\n          <p\n            className=\"px-1 py-2 text-center text-[11px] text-muted-foreground\"\n            data-thread-bubble-state=\"empty\"\n          >\n            {resolvedLabels.empty}\n          </p>\n        ) : (\n          <ul className=\"space-y-2 overflow-y-auto\" data-thread-bubble-list>\n            {messages.map((message) => (\n              <Message key={message.id} message={message} />\n            ))}\n          </ul>\n        )}\n        {footer ? (\n          <footer\n            className=\"border-t border-border pt-2\"\n            data-thread-bubble-footer\n          >\n            {footer}\n          </footer>\n        ) : null}\n      </section>\n    );\n  },\n);\nThreadBubble.displayName = \"ThreadBubble\";\n",
      "type": "registry:component"
    }
  ],
  "version": "0.2.1",
  "stability": "stable"
}
