{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "conversation-thread",
  "title": "Conversation Thread",
  "description": "Messages, cited sources, response states, and a prompt composer in one thread.",
  "dependencies": [],
  "registryDependencies": [
    "https://uaiblocks.dev/r/uai-theme.json",
    "https://uaiblocks.dev/r/uai-utils.json",
    "https://uaiblocks.dev/r/citation.json",
    "https://uaiblocks.dev/r/message.json",
    "https://uaiblocks.dev/r/prompt-composer.json",
    "https://uaiblocks.dev/r/response-status.json"
  ],
  "files": [
    {
      "path": "src/registry/uai/blocks/conversation-thread.tsx",
      "content": "\"use client\";\n\nimport {\n  type ComponentProps,\n  createContext,\n  type UIEvent,\n  useContext,\n  useEffect,\n  useId,\n  useRef,\n} from \"react\";\nimport { Citation, type CitationProps, type CitationVariant } from \"@/components/ui/uai/citation\";\nimport { Message, type MessageProps, type MessageVariant } from \"@/components/ui/uai/message\";\nimport {\n  PromptComposer,\n  type PromptComposerProps,\n  type PromptComposerVariant,\n} from \"@/components/ui/uai/prompt-composer\";\nimport {\n  ResponseStatus,\n  type ResponseStatusProps,\n  type ResponseStatusVariant,\n} from \"@/components/ui/uai/response-status\";\nimport { cn } from \"@/lib/uai-utils\";\n\nexport const CONVERSATION_THREAD_VARIANTS = [\"chat\", \"document\", \"compact\"] as const;\nexport type ConversationThreadVariant = (typeof CONVERSATION_THREAD_VARIANTS)[number];\nexport type ConversationThreadProps = ComponentProps<\"section\"> & {\n  variant?: ConversationThreadVariant;\n};\n\ntype ThreadContext = { id: string; variant: ConversationThreadVariant };\nconst Context = createContext<ThreadContext | null>(null);\nfunction useThread(part: string) {\n  const context = useContext(Context);\n  if (!context) throw new Error(`${part} must be used within ConversationThread`);\n  return context;\n}\nconst SourcesContext = createContext<string | null>(null);\n\nconst messageVariants: Record<ConversationThreadVariant, MessageVariant> = {\n  chat: \"bubble\",\n  document: \"plain\",\n  compact: \"compact\",\n};\nconst citationVariants: Record<ConversationThreadVariant, CitationVariant> = {\n  chat: \"number\",\n  document: \"underline\",\n  compact: \"number\",\n};\nconst statusVariants: Record<ConversationThreadVariant, ResponseStatusVariant> = {\n  chat: \"pill\",\n  document: \"inline\",\n  compact: \"inline\",\n};\nconst composerVariants: Record<ConversationThreadVariant, PromptComposerVariant> = {\n  chat: \"rounded\",\n  document: \"rounded\",\n  compact: \"compact\",\n};\n\n/** A conversation with messages, sources, response state, and a composer. Sources move beside the thread at 760px. */\nexport function ConversationThread({\n  variant = \"chat\",\n  children,\n  className,\n  ...props\n}: ConversationThreadProps) {\n  const id = useId();\n  return (\n    <Context.Provider value={{ id, variant }}>\n      <section\n        aria-labelledby={`${id}-title`}\n        data-slot=\"conversation-thread\"\n        data-variant={variant}\n        className={cn(\n          \"box-border @container min-w-0 text-[13px]/[18px] text-foreground\",\n          className,\n        )}\n        {...props}\n      >\n        <div\n          className={cn(\n            \"grid min-w-0 items-start\",\n            variant === \"compact\"\n              ? \"gap-3\"\n              : \"gap-4 @min-[760px]:grid-cols-[minmax(0,1fr)_minmax(220px,260px)] @min-[760px]:gap-6\",\n          )}\n        >\n          {children}\n        </div>\n      </section>\n    </Context.Provider>\n  );\n}\n\nexport function ConversationThreadHeader({ className, ...props }: ComponentProps<\"header\">) {\n  useThread(\"ConversationThreadHeader\");\n  return (\n    <header\n      data-slot=\"conversation-thread-header\"\n      className={cn(\"grid min-w-0 gap-1 @min-[760px]:col-span-full\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function ConversationThreadTitle({ className, ...props }: ComponentProps<\"h2\">) {\n  const { id, variant } = useThread(\"ConversationThreadTitle\");\n  return (\n    <h2\n      data-slot=\"conversation-thread-title\"\n      className={cn(\n        \"m-0 font-semibold tracking-[-0.01em] wrap-anywhere\",\n        variant === \"compact\" ? \"text-[15px]/5\" : \"text-lg/6\",\n        className,\n      )}\n      {...props}\n      id={`${id}-title`}\n    />\n  );\n}\n\nexport function ConversationThreadDescription({ className, ...props }: ComponentProps<\"p\">) {\n  return (\n    <p\n      data-slot=\"conversation-thread-description\"\n      className={cn(\"m-0 text-pretty text-muted-foreground\", className)}\n      {...props}\n    />\n  );\n}\n\n/** The conversation column: the log, the response status, and the composer. */\nexport function ConversationThreadMain({ className, ...props }: ComponentProps<\"div\">) {\n  const { variant } = useThread(\"ConversationThreadMain\");\n  return (\n    <div\n      data-slot=\"conversation-thread-main\"\n      className={cn(\n        \"grid min-w-0 content-start\",\n        variant === \"compact\" ? \"gap-2\" : \"gap-3\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\n/** A scrollable `role=\"log\"` region. It stays pinned to the newest message unless the reader scrolls up. */\nexport function ConversationThreadLog({ onScroll, className, ...props }: ComponentProps<\"div\">) {\n  const { id, variant } = useThread(\"ConversationThreadLog\");\n  const ref = useRef<HTMLDivElement>(null);\n  const pinned = useRef(true);\n  const messageCount = useRef<number | null>(null);\n  const smoothing = useRef(false);\n  // A new message glides into view; streaming text and reduced motion stay instant.\n  useEffect(() => {\n    const node = ref.current;\n    if (!node) return;\n    const previous = messageCount.current;\n    messageCount.current = node.childElementCount;\n    if (!pinned.current) return;\n    const appended = previous !== null && node.childElementCount > previous;\n    const reduce =\n      typeof window.matchMedia === \"function\" &&\n      window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    if ((appended || smoothing.current) && !reduce && typeof node.scrollTo === \"function\") {\n      smoothing.current = true;\n      node.scrollTo({ top: node.scrollHeight, behavior: \"smooth\" });\n    } else {\n      node.scrollTop = node.scrollHeight;\n    }\n  });\n  useEffect(() => {\n    const node = ref.current;\n    if (!node) return;\n    const settle = () => {\n      smoothing.current = false;\n      pinned.current = node.scrollHeight - node.scrollTop - node.clientHeight < 32;\n    };\n    node.addEventListener(\"scrollend\", settle);\n    return () => node.removeEventListener(\"scrollend\", settle);\n  }, []);\n  return (\n    <div\n      role=\"log\"\n      aria-live=\"polite\"\n      aria-relevant=\"additions\"\n      aria-labelledby={`${id}-title`}\n      // biome-ignore lint/a11y/noNoninteractiveTabindex: the scrollable log must be reachable by keyboard.\n      tabIndex={0}\n      data-slot=\"conversation-thread-log\"\n      className={cn(\n        \"grid min-w-0 content-start overflow-y-auto overscroll-contain rounded-xl\",\n        variant === \"compact\"\n          ? \"max-h-[360px] gap-3 px-0.5 py-1\"\n          : \"max-h-[520px] gap-5 px-1 pt-1 pb-2\",\n        className,\n      )}\n      {...props}\n      ref={ref}\n      onScroll={(event: UIEvent<HTMLDivElement>) => {\n        onScroll?.(event);\n        const node = event.currentTarget;\n        const distance = node.scrollHeight - node.scrollTop - node.clientHeight;\n        // Mid-glide positions are not the reader scrolling away.\n        if (smoothing.current) {\n          if (distance < 32) smoothing.current = false;\n          return;\n        }\n        pinned.current = distance < 32;\n      }}\n    />\n  );\n}\n\nexport type ConversationThreadMessageProps = Omit<MessageProps, \"variant\">;\n\n/** One turn in the conversation. Compose Message parts inside it. */\nexport function ConversationThreadMessage(props: ConversationThreadMessageProps) {\n  const { variant } = useThread(\"ConversationThreadMessage\");\n  return <Message {...props} variant={messageVariants[variant]} />;\n}\n\n/** An inline source marker inside a message. Compose Citation parts inside it. */\nexport function ConversationThreadCitation(props: Omit<CitationProps, \"variant\">) {\n  const { variant } = useThread(\"ConversationThreadCitation\");\n  return <Citation {...props} variant={citationVariants[variant]} />;\n}\n\n/** Where the latest response stands. Compose Response Status parts inside it. */\nexport function ConversationThreadStatus(props: Omit<ResponseStatusProps, \"variant\">) {\n  const { variant } = useThread(\"ConversationThreadStatus\");\n  return <ResponseStatus {...props} variant={statusVariants[variant]} />;\n}\n\n/** The prompt form. Compose Prompt Composer parts inside it. */\nexport function ConversationThreadComposer(props: Omit<PromptComposerProps, \"variant\">) {\n  const { variant } = useThread(\"ConversationThreadComposer\");\n  return <PromptComposer {...props} variant={composerVariants[variant]} />;\n}\n\n/** Sources referenced in the conversation. Sits beside the thread on wide containers. */\nexport function ConversationThreadSources({ className, ...props }: ComponentProps<\"aside\">) {\n  const { variant } = useThread(\"ConversationThreadSources\");\n  const id = useId();\n  const compact = variant === \"compact\";\n  return (\n    <SourcesContext.Provider value={id}>\n      <aside\n        aria-labelledby={id}\n        data-slot=\"conversation-thread-sources\"\n        className={cn(\n          \"grid min-w-0 content-start border bg-card\",\n          compact ? \"gap-1.5 rounded-xl p-3\" : \"gap-2.5 rounded-[14px] px-3 pt-3.5 pb-2.5\",\n          className,\n        )}\n        {...props}\n      />\n    </SourcesContext.Provider>\n  );\n}\n\nexport function ConversationThreadSourcesTitle({ className, ...props }: ComponentProps<\"h3\">) {\n  const id = useContext(SourcesContext);\n  if (!id)\n    throw new Error(\"ConversationThreadSourcesTitle must be used within ConversationThreadSources\");\n  return (\n    <h3\n      data-slot=\"conversation-thread-sources-title\"\n      className={cn(\"m-0 text-[13px]/[18px] font-medium\", className)}\n      {...props}\n      id={id}\n    />\n  );\n}\n\nexport function ConversationThreadSourceList({ className, ...props }: ComponentProps<\"ol\">) {\n  const id = useContext(SourcesContext);\n  if (!id)\n    throw new Error(\"ConversationThreadSourceList must be used within ConversationThreadSources\");\n  return (\n    <ol\n      aria-labelledby={id}\n      data-slot=\"conversation-thread-source-list\"\n      className={cn(\"m-0 grid min-w-0 list-none gap-0.5 p-0\", className)}\n      {...props}\n    />\n  );\n}\n\nexport type ConversationThreadSourceProps = ComponentProps<\"li\"> & {\n  /** Source number, matching the citation markers in the conversation. */\n  index: number;\n};\n\nexport function ConversationThreadSource({\n  index,\n  children,\n  className,\n  ...props\n}: ConversationThreadSourceProps) {\n  useThread(\"ConversationThreadSource\");\n  return (\n    <li\n      data-slot=\"conversation-thread-source\"\n      className={cn(\n        \"grid min-w-0 grid-cols-[20px_minmax(0,1fr)] items-start gap-2 rounded-lg p-1.5 transition-[background-color] duration-120 ease-out hover:bg-accent motion-reduce:transition-none\",\n        className,\n      )}\n      {...props}\n    >\n      <span\n        aria-hidden=\"true\"\n        className=\"mt-px grid h-[18px] place-items-center rounded-full bg-muted text-[11px] font-medium text-muted-foreground tabular-nums\"\n      >\n        {index}\n      </span>\n      <span className=\"grid min-w-0 gap-0.5\">{children}</span>\n    </li>\n  );\n}\n\nexport function ConversationThreadSourceLink({ className, ...props }: ComponentProps<\"a\">) {\n  return (\n    <a\n      data-slot=\"conversation-thread-source-link\"\n      className={cn(\n        \"font-medium wrap-anywhere text-foreground no-underline decoration-border-strong underline-offset-3 hover:underline focus-visible:rounded-[4px] focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-2 focus-visible:outline-ring\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function ConversationThreadSourceMeta({ className, ...props }: ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"conversation-thread-source-meta\"\n      className={cn(\"text-[12px] wrap-anywhere text-subtle-foreground\", className)}\n      {...props}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/uai/conversation-thread.tsx"
    }
  ],
  "categories": [
    "ai",
    "chat",
    "conversation"
  ],
  "type": "registry:block"
}