{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chatgpt-thread",
  "title": "ChatGPT Thread",
  "description": "The ChatGPT conversation column with auto-scroll, a scroll-to-bottom button and a pinned footer.",
  "dependencies": [
    "cn",
    "lucide-react"
  ],
  "registryDependencies": [
    "avatar",
    "button",
    "collapsible",
    "scroll-area",
    "sheet"
  ],
  "files": [
    {
      "path": "registry/notra/chatgpt/components/chatgpt-thread.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"cn\";\nimport { ArrowDownIcon } from \"lucide-react\";\nimport { useEffect, useRef, useState } from \"react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { ScrollArea } from \"@/components/ui/scroll-area\";\n\nimport { CHATGPT_SCROLL_STICK_THRESHOLD_PX } from \"../constants/chatgpt\";\nimport type { ChatgptThreadProps } from \"../types/chatgpt\";\n\nconst distanceFromBottom = (element: HTMLElement) =>\n  element.scrollHeight - element.scrollTop - element.clientHeight;\n\nconst getViewport = (root: HTMLDivElement | null) =>\n  root?.querySelector<HTMLDivElement>(\"[data-slot=scroll-area-viewport]\") ??\n  null;\n\nexport const ChatgptThread = ({\n  autoScroll = true,\n  children,\n  className,\n  footer,\n  ...props\n}: ChatgptThreadProps) => {\n  const scrollAreaRef = useRef<HTMLDivElement>(null);\n  const contentRef = useRef<HTMLDivElement>(null);\n  const stickRef = useRef(true);\n  const [atBottom, setAtBottom] = useState(true);\n\n  useEffect(() => {\n    const viewport = getViewport(scrollAreaRef.current);\n    if (!viewport) {\n      return;\n    }\n    const handleScroll = () => {\n      const nearBottom =\n        distanceFromBottom(viewport) <= CHATGPT_SCROLL_STICK_THRESHOLD_PX;\n      stickRef.current = nearBottom;\n      setAtBottom(nearBottom);\n    };\n    setAtBottom(\n      distanceFromBottom(viewport) <= CHATGPT_SCROLL_STICK_THRESHOLD_PX\n    );\n    viewport.addEventListener(\"scroll\", handleScroll, { passive: true });\n    return () => viewport.removeEventListener(\"scroll\", handleScroll);\n  }, []);\n\n  useEffect(() => {\n    const viewport = getViewport(scrollAreaRef.current);\n    const content = contentRef.current;\n    if (!(autoScroll && viewport && content)) {\n      return;\n    }\n    const observer = new ResizeObserver(() => {\n      if (stickRef.current) {\n        viewport.scrollTop = viewport.scrollHeight;\n      }\n      setAtBottom(\n        distanceFromBottom(viewport) <= CHATGPT_SCROLL_STICK_THRESHOLD_PX\n      );\n    });\n    observer.observe(content);\n    observer.observe(viewport);\n    return () => observer.disconnect();\n  }, [autoScroll]);\n\n  const scrollToBottom = () => {\n    const viewport = getViewport(scrollAreaRef.current);\n    if (!viewport) {\n      return;\n    }\n    stickRef.current = true;\n    const reduceMotion = window.matchMedia(\n      \"(prefers-reduced-motion: reduce)\"\n    ).matches;\n    viewport.scrollTo({\n      behavior: reduceMotion ? \"auto\" : \"smooth\",\n      top: viewport.scrollHeight,\n    });\n  };\n\n  return (\n    <div\n      className={cn(\n        \"bg-chatgpt-bg font-chatgpt text-chatgpt-fg relative flex min-h-0 flex-col antialiased\",\n        className\n      )}\n      data-slot=\"chatgpt-thread\"\n      {...props}\n    >\n      <div className=\"relative flex min-h-0 flex-1 flex-col\">\n        <ScrollArea\n          className=\"min-h-0 flex-1 overscroll-contain\"\n          ref={scrollAreaRef}\n        >\n          <div\n            className=\"mx-auto flex w-full max-w-3xl flex-col gap-10 px-4 py-8\"\n            ref={contentRef}\n          >\n            {children}\n          </div>\n        </ScrollArea>\n        {!atBottom && (\n          <Button\n            aria-label=\"Scroll to bottom\"\n            className=\"border-chatgpt-border bg-chatgpt-bg text-chatgpt-fg hover:bg-chatgpt-hover hover:text-chatgpt-fg focus-visible:ring-chatgpt-focus/35 dark:bg-chatgpt-bg dark:hover:bg-chatgpt-hover absolute bottom-3 left-1/2 size-8 -translate-x-1/2 rounded-full shadow-sm transition-opacity duration-150 focus-visible:ring-2 motion-reduce:transition-none starting:opacity-0\"\n            onClick={scrollToBottom}\n            size=\"icon\"\n            variant=\"ghost\"\n          >\n            <ArrowDownIcon className=\"size-4\" />\n          </Button>\n        )}\n      </div>\n      {footer && (\n        <div className=\"mx-auto w-full max-w-3xl px-4 pb-4\">{footer}</div>\n      )}\n    </div>\n  );\n};\n",
      "type": "registry:component",
      "target": "components/chatgpt/components/chatgpt-thread.tsx"
    },
    {
      "path": "registry/notra/chatgpt/types/chatgpt.ts",
      "content": "import type { ComponentProps, ReactNode } from \"react\";\n\nimport type { Avatar } from \"@/components/ui/avatar\";\nimport type { Button } from \"@/components/ui/button\";\nimport type { Collapsible } from \"@/components/ui/collapsible\";\nimport type { Sheet } from \"@/components/ui/sheet\";\n\nexport type ChatgptModelId =\n  | \"latest\"\n  | \"sol\"\n  | \"terra\"\n  | \"luna\"\n  | \"gpt-5.5\"\n  | \"gpt-5.4\"\n  | \"gpt-5.4-mini\";\n\nexport type ChatgptEffortId =\n  | \"instant\"\n  | \"medium\"\n  | \"high\"\n  | \"extra-high\"\n  | \"pro\";\n\nexport type ChatgptMessageRole = \"user\" | \"assistant\";\n\nexport interface ChatgptModelOption {\n  description?: string;\n  id: ChatgptModelId;\n  label: string;\n  shortLabel: string;\n}\n\nexport interface ChatgptEffortOption {\n  id: ChatgptEffortId;\n  label: string;\n}\n\nexport interface ChatgptActivitySite {\n  domain: string;\n  favicon?: string;\n  label: string;\n}\n\nexport interface ChatgptActivitySource {\n  domain: string;\n  favicon?: string;\n  href?: string;\n  id: string;\n  publisher: string;\n  snippet: string;\n  timeLabel: string;\n  title: string;\n}\n\nexport interface ChatgptThreadProps extends ComponentProps<\"div\"> {\n  autoScroll?: boolean;\n  footer?: ReactNode;\n}\n\nexport interface ChatgptMessageProps extends ComponentProps<\"div\"> {\n  actions?: ReactNode;\n  from: ChatgptMessageRole;\n  reasoning?: ReactNode;\n}\n\nexport interface ChatgptReasoningProps extends Omit<\n  ComponentProps<typeof Collapsible>,\n  \"children\"\n> {\n  children?: ReactNode;\n  complete?: boolean;\n  search?: ReactNode;\n  seconds: number;\n}\n\nexport interface ChatgptSearchProps extends Omit<\n  ComponentProps<typeof Button>,\n  \"children\"\n> {\n  sites?: readonly ChatgptActivitySite[];\n  websites: number;\n}\n\nexport interface ChatgptActivityProps extends Omit<\n  ComponentProps<typeof Sheet>,\n  \"children\"\n> {\n  className?: string;\n  seconds: number;\n  sites: readonly ChatgptActivitySite[];\n  sourceCount?: number;\n  sources: readonly ChatgptActivitySource[];\n  websites: number;\n}\n\nexport interface ChatgptFaviconProps extends Omit<\n  ComponentProps<typeof Avatar>,\n  \"children\"\n> {\n  domain: string;\n  src?: string;\n}\n\nexport interface ChatgptThinkingProps extends ComponentProps<\"div\"> {\n  label?: ReactNode;\n}\n\nexport interface ChatgptAppCallProps extends ComponentProps<\"div\"> {\n  label?: ReactNode;\n  logo?: ReactNode;\n  name: ReactNode;\n}\n\nexport interface ChatgptActionsProps extends ComponentProps<\"div\"> {\n  onMore?: () => void;\n  onRedo?: () => void;\n  onShare?: () => void;\n  text: string;\n}\n\nexport interface ChatgptActionButtonProps extends ComponentProps<\n  typeof Button\n> {\n  label: string;\n}\n\nexport interface ChatgptModelSelectorProps extends Omit<\n  ComponentProps<typeof Button>,\n  \"children\"\n> {\n  effort: ChatgptEffortId;\n  efforts?: readonly ChatgptEffortOption[];\n  model: ChatgptModelId;\n  models?: readonly ChatgptModelOption[];\n  onEffortChange?: (effort: ChatgptEffortId) => void;\n  onModelChange?: (model: ChatgptModelId) => void;\n}\n\nexport interface ChatgptComposerProps extends Omit<\n  ComponentProps<\"form\">,\n  \"onSubmit\"\n> {\n  busy?: boolean;\n  defaultEffort?: ChatgptEffortId;\n  defaultModel?: ChatgptModelId;\n  effort?: ChatgptEffortId;\n  efforts?: readonly ChatgptEffortOption[];\n  model?: ChatgptModelId;\n  models?: readonly ChatgptModelOption[];\n  onEffortChange?: (effort: ChatgptEffortId) => void;\n  onModelChange?: (model: ChatgptModelId) => void;\n  onSend?: (text: string) => void;\n  onStop?: () => void;\n  placeholder?: string;\n}\n\nexport interface ChatgptStorySearch {\n  sites: readonly ChatgptActivitySite[];\n  sourceCount?: number;\n  sources: readonly ChatgptActivitySource[];\n  websites: number;\n}\n\nexport type ChatgptStoryReasoningStep =\n  | { kind: \"search\"; search: ChatgptStorySearch }\n  | { kind: \"text\"; muted?: boolean; text: string };\n\nexport interface ChatgptStoryReasoning {\n  search?: ChatgptStorySearch;\n  seconds: number;\n  steps?: readonly ChatgptStoryReasoningStep[];\n  text: string;\n}\n\nexport interface ChatgptStoryMessage {\n  from: ChatgptMessageRole;\n  id: string;\n  reasoning?: ChatgptStoryReasoning;\n  text: string;\n}\n\nexport interface ChatgptPlayback {\n  completeIds: ReadonlySet<string>;\n  messages: ChatgptStoryMessage[];\n  play: () => Promise<void>;\n  playing: boolean;\n  send: (text: string, reply: string) => Promise<void>;\n  stop: () => void;\n  thinking: boolean;\n}\n\nexport interface ChatgptTextSegment {\n  offset: number;\n  text: string;\n}\n\nexport interface ChatgptSourceChipProps extends Omit<\n  ComponentProps<\"a\">,\n  \"children\" | \"href\"\n> {\n  sources: readonly ChatgptActivitySource[];\n}\n\nexport type ChatgptPlaybackPhase =\n  | { status: \"idle\" }\n  | { status: \"playing\"; source: \"script\" | \"send\"; thinking: boolean };\n",
      "type": "registry:lib",
      "target": "components/chatgpt/types/chatgpt.ts"
    },
    {
      "path": "registry/notra/chatgpt/constants/chatgpt.ts",
      "content": "export const CHATGPT_TOOLTIP_DELAY_MS = 200;\n\nexport const CHATGPT_COPIED_RESET_MS = 1500;\n\nexport const CHATGPT_VISIBLE_SITES = 3;\n\nexport const CHATGPT_SCROLL_STICK_THRESHOLD_PX = 48;\n\nexport const CHATGPT_FAVICON_URL = \"https://www.google.com/s2/favicons\";\n\nexport const CHATGPT_PLAYBACK_TIMING = {\n  afterAssistantMs: 480,\n  afterInstantMs: 180,\n  afterUserMs: 420,\n  reducedMaxMs: 70,\n  thinkingMs: 1100,\n  thinkingWithReasoningMs: 1600,\n  tokenMs: 28,\n} as const;\n\nexport const CHATGPT_PRO_SPARKLES = [\n  { delay: 0, size: 4, x: -30, y: -26 },\n  { delay: 30, size: 3, x: 0, y: -34 },\n  { delay: 10, size: 4, x: 30, y: -24 },\n  { delay: 50, size: 3, x: -38, y: 2 },\n  { delay: 20, size: 3, x: 38, y: 4 },\n  { delay: 40, size: 4, x: -28, y: 26 },\n  { delay: 0, size: 3, x: 2, y: 34 },\n  { delay: 60, size: 4, x: 28, y: 26 },\n  { delay: 80, size: 2, x: -18, y: -14 },\n  { delay: 70, size: 2, x: 18, y: 16 },\n] as const;\n\nexport const CHATGPT_PRO_TWINKLES = [\n  { delay: 0, duration: 3000, size: 2, x: 6, y: 32 },\n  { delay: 900, duration: 3600, size: 3, x: 15, y: 66 },\n  { delay: 400, duration: 2800, size: 2, x: 24, y: 28 },\n  { delay: 1500, duration: 3400, size: 2, x: 33, y: 60 },\n  { delay: 700, duration: 3200, size: 3, x: 43, y: 36 },\n  { delay: 2100, duration: 3800, size: 2, x: 52, y: 70 },\n  { delay: 300, duration: 3000, size: 2, x: 61, y: 30 },\n  { delay: 1200, duration: 3500, size: 3, x: 70, y: 62 },\n  { delay: 1800, duration: 2900, size: 2, x: 79, y: 34 },\n  { delay: 600, duration: 3300, size: 2, x: 87, y: 64 },\n] as const;\n\nexport const CHATGPT_CHIP_HOVER_OPEN_DELAY_MS = 150;\n\nexport const CHATGPT_CHIP_HOVER_CLOSE_DELAY_MS = 120;\n",
      "type": "registry:lib",
      "target": "components/chatgpt/constants/chatgpt.ts"
    }
  ],
  "cssVars": {
    "theme": {
      "color-chatgpt-bg": "var(--chatgpt-bg)",
      "color-chatgpt-fg": "var(--chatgpt-fg)",
      "color-chatgpt-muted": "var(--chatgpt-muted)",
      "color-chatgpt-hover": "var(--chatgpt-hover)",
      "color-chatgpt-border": "var(--chatgpt-border)",
      "color-chatgpt-bubble": "var(--chatgpt-bubble)",
      "color-chatgpt-composer": "var(--chatgpt-composer)",
      "color-chatgpt-composer-border": "var(--chatgpt-composer-border)",
      "color-chatgpt-popover": "var(--chatgpt-popover)",
      "color-chatgpt-tooltip": "var(--chatgpt-tooltip)",
      "color-chatgpt-tooltip-fg": "var(--chatgpt-tooltip-fg)",
      "color-chatgpt-send": "var(--chatgpt-send)",
      "color-chatgpt-send-hover": "var(--chatgpt-send-hover)",
      "color-chatgpt-send-fg": "var(--chatgpt-send-fg)",
      "color-chatgpt-focus": "var(--chatgpt-focus)",
      "color-chatgpt-search": "var(--chatgpt-search)",
      "shadow-chatgpt-composer": "var(--chatgpt-composer-shadow)",
      "shadow-chatgpt-menu": "var(--chatgpt-menu-shadow)",
      "font-chatgpt": "ui-sans-serif, -apple-system, system-ui, \"Segoe UI\", Helvetica, \"Apple Color Emoji\", Arial, sans-serif, \"Segoe UI Emoji\", \"Segoe UI Symbol\"",
      "color-chatgpt-slider-track": "var(--chatgpt-slider-track)",
      "color-chatgpt-slider-fill": "var(--chatgpt-slider-fill)",
      "color-chatgpt-slider-thumb": "var(--chatgpt-slider-thumb)",
      "color-chatgpt-slider-dot": "var(--chatgpt-slider-dot)",
      "color-chatgpt-slider-dot-fill": "var(--chatgpt-slider-dot-fill)",
      "color-chatgpt-pro": "var(--chatgpt-pro)"
    },
    "light": {
      "chatgpt-bg": "#ffffff",
      "chatgpt-fg": "#0d0d0d",
      "chatgpt-muted": "#5d5d5d",
      "chatgpt-hover": "#f3f3f3",
      "chatgpt-border": "#e5e5e5",
      "chatgpt-bubble": "#f4f4f4",
      "chatgpt-composer": "#ffffff",
      "chatgpt-composer-border": "#00000014",
      "chatgpt-composer-shadow": "0 1px 2px rgb(0 0 0 / 0.04), 0 2px 8px rgb(0 0 0 / 0.04)",
      "chatgpt-popover": "#ffffff",
      "chatgpt-menu-shadow": "0 0 0 1px rgb(0 0 0 / 0.05), 0 4px 16px rgb(0 0 0 / 0.08)",
      "chatgpt-tooltip": "#0d0d0d",
      "chatgpt-tooltip-fg": "#ffffff",
      "chatgpt-send": "#2563eb",
      "chatgpt-send-hover": "#1d4ed8",
      "chatgpt-send-fg": "#ffffff",
      "chatgpt-focus": "#2563eb",
      "chatgpt-search": "#e4543a",
      "chatgpt-slider-track": "#e4e4e4",
      "chatgpt-slider-fill": "#3b82f6",
      "chatgpt-slider-thumb": "#ffffff",
      "chatgpt-slider-dot": "#a3a3a3",
      "chatgpt-slider-dot-fill": "rgb(255 255 255 / 60%)",
      "chatgpt-pro": "#7c3aed",
      "chatgpt-pro-gradient": "linear-gradient(90deg, #2b3fd6 0%, #6a5be2 25%, #b58cf5 50%, #6a5be2 75%, #2b3fd6 100%)"
    },
    "dark": {
      "chatgpt-bg": "#212121",
      "chatgpt-fg": "#ececec",
      "chatgpt-muted": "#afafaf",
      "chatgpt-hover": "#303030",
      "chatgpt-border": "#3a3a3a",
      "chatgpt-bubble": "#303030",
      "chatgpt-composer": "#303030",
      "chatgpt-composer-border": "#ffffff1a",
      "chatgpt-composer-shadow": "0 1px 2px rgb(0 0 0 / 0.2)",
      "chatgpt-popover": "#353535",
      "chatgpt-menu-shadow": "0 0 0 1px rgb(255 255 255 / 0.08), 0 8px 24px rgb(0 0 0 / 0.4)",
      "chatgpt-tooltip": "#000000",
      "chatgpt-tooltip-fg": "#ffffff",
      "chatgpt-send": "#2563eb",
      "chatgpt-send-hover": "#1d4ed8",
      "chatgpt-send-fg": "#ffffff",
      "chatgpt-focus": "#2563eb",
      "chatgpt-search": "#e4543a",
      "chatgpt-slider-track": "#454545",
      "chatgpt-slider-fill": "#3b82f6",
      "chatgpt-slider-thumb": "#ffffff",
      "chatgpt-slider-dot": "#8c8c8c",
      "chatgpt-slider-dot-fill": "rgb(255 255 255 / 55%)",
      "chatgpt-pro": "#b794f6",
      "chatgpt-pro-gradient": "linear-gradient(90deg, #2b3fd6 0%, #6a5be2 25%, #b58cf5 50%, #6a5be2 75%, #2b3fd6 100%)"
    }
  },
  "type": "registry:component"
}