{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "thread-bubble",
  "title": "Thread Bubble",
  "description": "Expanded discussion bubble for an anchored canvas comment thread.",
  "dependencies": [
    "@vllnt/ui@^0.3.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/thread-bubble/thread-bubble.tsx",
      "content": "\"use client\";\n\nimport type { ComponentPropsWithoutRef, ReactNode } 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 = ({\n  ref,\n  ...props\n}: ThreadBubbleProps & { ref?: React.Ref<HTMLElement> }) => {\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};\nThreadBubble.displayName = \"ThreadBubble\";\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component",
  "version": "0.3.0",
  "stability": "stable"
}
