{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chat-bot",
  "title": "Chat Bot",
  "description": "Simulated AI chatbot with animated typing, message bubbles, a three-dot thinking indicator, and a looping Q&A exchange. No avatars â€” pure bubble UI.",
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/new-york/blocks/chat-bot/page.tsx",
      "content": "import ChatBot from \"./components/chat-bot\"\n\nexport default function Page() {\n  return (\n    <div className=\"px-4 py-10 md:px-6 md:py-14\">\n      <ChatBot />\n    </div>\n  )\n}\n",
      "type": "registry:page",
      "target": "app/chat-bot/page.tsx"
    },
    {
      "path": "registry/new-york/blocks/chat-bot/components/chat-bot.tsx",
      "content": "\"use client\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n  RotateCcw,\n  Copy,\n  Share2,\n  ThumbsUp,\n  ThumbsDown,\n  CornerDownRight,\n} from \"lucide-react\"\nimport { useChatAnimation } from \"../hooks/use-chat-animation\"\nimport MessageBubble from \"./message-bubble\"\nimport ThinkLabel from \"./typing-indicator\"\nimport ChatInput from \"./chat-input\"\nimport { SUGGESTIONS } from \"../lib/config\"\nimport \"../styles/chat-bot.css\"\n\nexport default function ChatBot({ className }: { className?: string }) {\n  const { phase, inputText, userMessage, botText, thinkSeconds, settled } =\n    useChatAnimation()\n  const showMessages = userMessage !== null\n  const isPaused = phase === \"paused\"\n\n  return (\n    <div\n      className={cn(\n        \"cb-root mx-auto flex w-full max-w-3xl min-w-0 justify-center px-6 py-12\",\n        className\n      )}\n    >\n      <div className=\"flex w-full max-w-[600px] flex-col gap-7\">\n        <div\n          className={cn(\n            \"pointer-events-none flex translate-y-5 flex-col gap-6 opacity-0 transition-[opacity,transform] duration-500 ease-[cubic-bezier(0.16,1,0.3,1)] motion-reduce:transition-none\",\n            showMessages && \"pointer-events-auto translate-y-0 opacity-100\"\n          )}\n        >\n          {userMessage && <MessageBubble role=\"user\" text={userMessage} />}\n\n          <div className=\"flex flex-col gap-2.5\">\n            <ThinkLabel phase={phase} thinkSeconds={thinkSeconds} />\n\n            {botText !== null && (\n              <MessageBubble\n                role=\"bot\"\n                text={botText}\n                isTyping={phase === \"botTyping\"}\n              />\n            )}\n\n            <div className={cn(\"cb-reveal\", isPaused && \"cb-reveal--open\")}>\n              <div className=\"cb-reveal-inner\">\n                {isPaused && (\n                  <>\n                    <div className=\"flex items-center gap-0.5\">\n                      <button\n                        type=\"button\"\n                        className=\"cb-action-btn flex size-9 cursor-default items-center justify-center rounded-2.5 border-none bg-transparent text-(--cb-action-color) transition-[color,background] duration-150 hover:bg-(--cb-action-hover-bg) hover:text-(--cb-action-hover-color)\"\n                        aria-label=\"Retry\"\n                      >\n                        <RotateCcw size={14} />\n                      </button>\n                      <button\n                        type=\"button\"\n                        className=\"cb-action-btn flex size-9 cursor-default items-center justify-center rounded-2.5 border-none bg-transparent text-(--cb-action-color) transition-[color,background] duration-150 hover:bg-(--cb-action-hover-bg) hover:text-(--cb-action-hover-color)\"\n                        aria-label=\"Copy\"\n                      >\n                        <Copy size={14} />\n                      </button>\n                      <button\n                        type=\"button\"\n                        className=\"cb-action-btn flex size-9 cursor-default items-center justify-center rounded-2.5 border-none bg-transparent text-(--cb-action-color) transition-[color,background] duration-150 hover:bg-(--cb-action-hover-bg) hover:text-(--cb-action-hover-color)\"\n                        aria-label=\"Share\"\n                      >\n                        <Share2 size={14} />\n                      </button>\n                      <button\n                        type=\"button\"\n                        className=\"cb-action-btn flex size-9 cursor-default items-center justify-center rounded-2.5 border-none bg-transparent text-(--cb-action-color) transition-[color,background] duration-150 hover:bg-(--cb-action-hover-bg) hover:text-(--cb-action-hover-color)\"\n                        aria-label=\"Thumbs up\"\n                      >\n                        <ThumbsUp size={14} />\n                      </button>\n                      <button\n                        type=\"button\"\n                        className=\"cb-action-btn flex size-9 cursor-default items-center justify-center rounded-2.5 border-none bg-transparent text-(--cb-action-color) transition-[color,background] duration-150 hover:bg-(--cb-action-hover-bg) hover:text-(--cb-action-hover-color)\"\n                        aria-label=\"Thumbs down\"\n                      >\n                        <ThumbsDown size={14} />\n                      </button>\n                    </div>\n                    <div className=\"flex flex-col gap-1.5\">\n                      {SUGGESTIONS.map((s) => (\n                        <button\n                          key={s}\n                          type=\"button\"\n                          className=\"cb-chip flex w-fit cursor-default items-center gap-2 rounded-3 border border-(--cb-chip-border) bg-transparent px-2.5 py-2 pr-3.5 text-left text-[11px] text-(--cb-chip-text) transition-[border-color] duration-150 hover:border-(--cb-chip-border)\"\n                        >\n                          <CornerDownRight\n                            size={12}\n                            className=\"shrink-0 text-(--cb-chip-icon)\"\n                            aria-hidden\n                          />\n                          {s}\n                        </button>\n                      ))}\n                    </div>\n                  </>\n                )}\n              </div>\n            </div>\n          </div>\n        </div>\n\n        <ChatInput value={inputText} phase={phase} settled={settled} />\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "app/chat-bot/components/chat-bot.tsx"
    },
    {
      "path": "registry/new-york/blocks/chat-bot/components/message-bubble.tsx",
      "content": "interface MessageBubbleProps {\n  role: \"user\" | \"bot\"\n  text: string\n  isTyping?: boolean\n}\n\nexport default function MessageBubble({\n  role,\n  text,\n  isTyping,\n}: MessageBubbleProps) {\n  if (role === \"user\") {\n    return (\n      <div className=\"flex justify-end\">\n        <div className=\"cb-user-bubble max-w-[80%] rounded-full bg-(--cb-user-bubble-bg) px-4.5 py-2.5 text-sm leading-normal text-(--cb-user-bubble-text)\">\n          {text}\n        </div>\n      </div>\n    )\n  }\n\n  return (\n    <p className=\"m-0 text-[13px] leading-[1.65] tracking-[0.01em] whitespace-pre-line text-(--cb-bot-text)\">\n      {text}\n      {isTyping ? (\n        <span\n          className=\"cb-bot-cursor ml-1.5 inline-block h-[1.05em] w-px shrink-0 translate-y-[0.1em] rounded-full bg-(--cb-bot-text) align-baseline\"\n          aria-hidden\n        />\n      ) : null}\n    </p>\n  )\n}\n",
      "type": "registry:component",
      "target": "app/chat-bot/components/message-bubble.tsx"
    },
    {
      "path": "registry/new-york/blocks/chat-bot/components/typing-indicator.tsx",
      "content": "import { Lightbulb } from \"lucide-react\"\nimport type { Phase } from \"../hooks/use-chat-animation\"\n\ninterface ThinkLabelProps {\n  phase: Phase\n  thinkSeconds: number\n}\n\nexport default function ThinkLabel({ phase, thinkSeconds }: ThinkLabelProps) {\n  if (phase === \"botThinking\") {\n    return (\n      <div className=\"flex items-center gap-1.5\">\n        <Lightbulb\n          size={13}\n          className=\"cb-think-pulse shrink-0 text-(--cb-think-color)\"\n          aria-hidden\n        />\n        <span className=\"text-[11px] text-(--cb-think-color)\">\n          Thinking…\n        </span>\n      </div>\n    )\n  }\n\n  if (phase === \"botTyping\" || phase === \"paused\") {\n    return (\n      <div className=\"flex items-center gap-1.5\">\n        <Lightbulb\n          size={13}\n          className=\"shrink-0 text-(--cb-think-color)\"\n          aria-hidden\n        />\n        <span className=\"text-[11px] text-(--cb-think-color)\">\n          Thought for {thinkSeconds}s\n        </span>\n      </div>\n    )\n  }\n\n  return null\n}\n",
      "type": "registry:component",
      "target": "app/chat-bot/components/typing-indicator.tsx"
    },
    {
      "path": "registry/new-york/blocks/chat-bot/components/chat-input.tsx",
      "content": "import { cn } from \"@/lib/utils\"\nimport { Paperclip, ArrowUp } from \"lucide-react\"\nimport type { Phase } from \"../hooks/use-chat-animation\"\n\ninterface ChatInputProps {\n  value: string\n  phase: Phase\n  settled: boolean\n}\n\nexport default function ChatInput({ value, phase, settled }: ChatInputProps) {\n  const isSending = phase === \"userSending\"\n  const isLoading =\n    isSending ||\n    phase === \"botThinking\" ||\n    phase === \"botTyping\" ||\n    (phase === \"paused\" && !settled)\n  const showCursor = phase === \"userTyping\"\n\n  return (\n    <div\n      className={cn(\n        \"flex items-center gap-2.5 rounded-full border border-(--cb-input-border) bg-(--cb-input-bg) py-2.5 pr-2.5 pl-4 transition-[border-color,opacity] duration-150\",\n        isSending && \"opacity-50\"\n      )}\n    >\n      <Paperclip\n        size={16}\n        className=\"shrink-0 text-(--cb-input-icon)\"\n        aria-hidden\n      />\n      <div className=\"flex min-w-0 flex-1 items-center\">\n        {value ? (\n          <span className=\"truncate text-sm leading-normal text-(--cb-input-text)\">\n            {value}\n          </span>\n        ) : (\n          <span className=\"text-sm leading-normal text-(--cb-input-placeholder) select-none\">\n            Message…\n          </span>\n        )}\n        {showCursor && (\n          <span\n            className=\"cb-input-cursor ml-px inline-block h-3.5 w-0.5 translate-y-px rounded-0.5 bg-(--cb-input-cursor) align-text-bottom\"\n            aria-hidden\n          />\n        )}\n      </div>\n      <div className=\"flex shrink-0 items-center gap-1.5\">\n        <button\n          type=\"button\"\n          className={cn(\n            \"flex size-8 shrink-0 cursor-default items-center justify-center rounded-full border-none bg-(--cb-send-bg) text-(--cb-send-text) transition-[opacity,transform] duration-150\",\n            !isSending && \"opacity-[0.28]\",\n            isSending && \"cb-send-btn-active opacity-100\"\n          )}\n          aria-label=\"Send message\"\n          tabIndex={-1}\n        >\n          {isLoading ? (\n            <span\n              className=\"cb-spinner block size-3.5 rounded-full border-2 border-current/25 border-t-current\"\n              aria-hidden\n            />\n          ) : (\n            <ArrowUp size={15} strokeWidth={2.5} />\n          )}\n        </button>\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "app/chat-bot/components/chat-input.tsx"
    },
    {
      "path": "registry/new-york/blocks/chat-bot/hooks/use-chat-animation.ts",
      "content": "import { useReducer, useLayoutEffect, useMemo, useRef } from \"react\"\n\nimport {\n  EXCHANGE,\n  TYPE_CHAR_MS,\n  PRE_SEND_DELAY_MS,\n  SEND_FLASH_MS,\n  BOT_THINK_MS,\n  BOT_TYPE_CHAR_MS,\n  LOOP_PAUSE_MS,\n} from \"../lib/config\"\n\nexport type Phase =\n  | \"userTyping\"\n  | \"userSending\"\n  | \"botThinking\"\n  | \"botTyping\"\n  | \"paused\"\n\ntype State = {\n  phase: Phase\n  charIdx: number\n  userSent: boolean\n  thinkSeconds: number\n  settled: boolean\n}\n\ntype Action =\n  | { type: \"ADVANCE_CHAR\" }\n  | { type: \"SET_PHASE\"; phase: Phase; charIdx?: number; userSent?: boolean }\n  | { type: \"TICK_THINK\" }\n  | { type: \"SET_SETTLED\" }\n  | { type: \"RESET\" }\n\nconst INITIAL: State = { phase: \"userTyping\", charIdx: 0, userSent: false, thinkSeconds: 0, settled: false }\n\nfunction reducer(state: State, action: Action): State {\n  switch (action.type) {\n    case \"ADVANCE_CHAR\":\n      return { ...state, charIdx: state.charIdx + 1 }\n    case \"SET_PHASE\":\n      return {\n        phase: action.phase,\n        charIdx: action.charIdx ?? state.charIdx,\n        userSent: action.userSent ?? state.userSent,\n        thinkSeconds: action.phase === \"botThinking\" ? 0 : state.thinkSeconds,\n        settled: false,\n      }\n    case \"TICK_THINK\":\n      return { ...state, thinkSeconds: state.thinkSeconds + 1 }\n    case \"SET_SETTLED\":\n      return { ...state, settled: true }\n    case \"RESET\":\n      return INITIAL\n    default:\n      return state\n  }\n}\n\nexport function useChatAnimation() {\n  const [state, dispatch] = useReducer(reducer, INITIAL)\n  const { phase, charIdx, userSent, thinkSeconds, settled } = state\n  const scrollRef = useRef<HTMLDivElement>(null)\n\n  const { question, answer } = EXCHANGE\n\n  // ── Derived display values ─────────────────────────────────────────────────\n\n  const inputText = useMemo(() => {\n    if (phase === \"userTyping\") return question.slice(0, charIdx)\n    if (phase === \"userSending\") return question\n    return \"\"\n  }, [phase, charIdx, question])\n\n  const userMessage: string | null = userSent ? question : null\n\n  const botText = useMemo((): string | null => {\n    if (phase === \"botTyping\") return answer.slice(0, charIdx)\n    if (phase === \"paused\") return answer\n    return null\n  }, [phase, charIdx, answer])\n\n  // ── Animation driver ───────────────────────────────────────────────────────\n\n  useLayoutEffect(() => {\n    let t: ReturnType<typeof setTimeout>\n\n    switch (phase) {\n      case \"userTyping\":\n        if (charIdx < question.length) {\n          t = setTimeout(() => dispatch({ type: \"ADVANCE_CHAR\" }), TYPE_CHAR_MS)\n        } else {\n          t = setTimeout(\n            () => dispatch({ type: \"SET_PHASE\", phase: \"userSending\" }),\n            PRE_SEND_DELAY_MS,\n          )\n        }\n        break\n\n      case \"userSending\":\n        t = setTimeout(\n          () =>\n            dispatch({\n              type: \"SET_PHASE\",\n              phase: \"botThinking\",\n              charIdx: 0,\n              userSent: true,\n            }),\n          SEND_FLASH_MS,\n        )\n        break\n\n      case \"botThinking\":\n        t = setTimeout(\n          () => dispatch({ type: \"SET_PHASE\", phase: \"botTyping\", charIdx: 0 }),\n          BOT_THINK_MS,\n        )\n        break\n\n      case \"botTyping\":\n        if (charIdx < answer.length) {\n          t = setTimeout(() => dispatch({ type: \"ADVANCE_CHAR\" }), BOT_TYPE_CHAR_MS)\n        } else {\n          t = setTimeout(\n            () => dispatch({ type: \"SET_PHASE\", phase: \"paused\" }),\n            250,\n          )\n        }\n        break\n\n      case \"paused\":\n        t = setTimeout(() => dispatch({ type: \"RESET\" }), LOOP_PAUSE_MS)\n        break\n    }\n\n    return () => clearTimeout(t)\n  }, [phase, charIdx, question, answer])\n\n  // ── Think-seconds ticker ───────────────────────────────────────────────────\n\n  useLayoutEffect(() => {\n    if (phase !== \"botThinking\") return\n    const id = setInterval(() => dispatch({ type: \"TICK_THINK\" }), 1000)\n    return () => clearInterval(id)\n  }, [phase])\n\n  // ── Settle after chips animate in (~500ms into paused) ─────────────────────\n\n  useLayoutEffect(() => {\n    if (phase !== \"paused\") return\n    const t = setTimeout(() => dispatch({ type: \"SET_SETTLED\" }), 1150)\n    return () => clearTimeout(t)\n  }, [phase])\n\n  // ── Auto-scroll ────────────────────────────────────────────────────────────\n\n  useLayoutEffect(() => {\n    const el = scrollRef.current\n    if (!el) return\n    el.scrollTop = el.scrollHeight - el.clientHeight\n  }, [userMessage, botText, phase])\n\n  return { phase, inputText, userMessage, botText, scrollRef, thinkSeconds, settled }\n}\n",
      "type": "registry:hook",
      "target": "app/chat-bot/hooks/use-chat-animation.ts"
    },
    {
      "path": "registry/new-york/blocks/chat-bot/lib/config.ts",
      "content": "/** Typing speed for the simulated user input */\nexport const TYPE_CHAR_MS = 55\n\n/** Pause after the user finishes typing before \"sending\" */\nexport const PRE_SEND_DELAY_MS = 420\n\n/** How long the \"sending\" flash lasts */\nexport const SEND_FLASH_MS = 280\n\n/** How long the bot \"thinks\" before responding */\nexport const BOT_THINK_MS = 1100\n\n/** Typing speed for the bot response */\nexport const BOT_TYPE_CHAR_MS = 18\n\n/** How long the completed conversation sits before looping */\nexport const LOOP_PAUSE_MS = 3600\n\nexport const EXCHANGE = {\n  question: \"Why was my transfer flagged?\",\n  answer: \"Your $4,200 transfer to a new payee triggered a routine compliance check. It's been reviewed and cleared — funds will arrive by end of day. No action needed on your end.\",\n}\n\nexport const SUGGESTIONS = [\n  \"Set this payee as trusted\",\n  \"View my transfer history\",\n] as const\n",
      "type": "registry:lib",
      "target": "app/chat-bot/lib/config.ts"
    },
    {
      "path": "registry/new-york/blocks/chat-bot/styles/chat-bot.css",
      "content": "/* chat-bot — theme tokens, reveal grid, keyframes, caret & stagger animations */\n\n.cb-root {\n  --cb-user-bubble-bg: #18181b;\n  --cb-user-bubble-text: #fafafa;\n  --cb-bot-text: #18181b;\n  --cb-think-color: #71717a;\n  --cb-input-bg: #f4f4f5;\n  --cb-input-border: #e4e4e7;\n  --cb-input-text: #18181b;\n  --cb-input-placeholder: #a1a1aa;\n  --cb-input-cursor: #71717a;\n  --cb-input-icon: #a1a1aa;\n  --cb-send-bg: #18181b;\n  --cb-send-text: #ffffff;\n  --cb-action-color: #a1a1aa;\n  --cb-action-hover-bg: #f4f4f5;\n  --cb-action-hover-color: #71717a;\n  --cb-chip-border: #e4e4e7;\n  --cb-chip-text: #71717a;\n  --cb-chip-icon: #a1a1aa;\n}\n\n.dark .cb-root {\n  --cb-user-bubble-bg: #1c1c1f;\n  --cb-user-bubble-text: #f4f4f5;\n  --cb-bot-text: #e4e4e7;\n  --cb-think-color: #71717a;\n  --cb-input-bg: #0d0d0d;\n  --cb-input-border: #2a2a2a;\n  --cb-input-text: #e4e4e7;\n  --cb-input-placeholder: #52525b;\n  --cb-input-cursor: #71717a;\n  --cb-input-icon: #52525b;\n  --cb-send-bg: #ffffff;\n  --cb-send-text: #000000;\n  --cb-action-color: #52525b;\n  --cb-action-hover-bg: #1c1c1f;\n  --cb-action-hover-color: #a1a1aa;\n  --cb-chip-border: #27272a;\n  --cb-chip-text: #a1a1aa;\n  --cb-chip-icon: #52525b;\n}\n\n.cb-reveal {\n  display: grid;\n  grid-template-rows: 0fr;\n  transition: grid-template-rows 0.5s cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.cb-reveal--open {\n  grid-template-rows: 1fr;\n}\n\n.cb-reveal-inner {\n  overflow: hidden;\n  display: flex;\n  flex-direction: column;\n  gap: 14px;\n  padding-top: 0;\n  transition: padding-top 0.5s cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.cb-reveal--open .cb-reveal-inner {\n  padding-top: 14px;\n}\n\n.cb-user-bubble {\n  animation: cb-bubble-in 0.45s cubic-bezier(0.16, 1, 0.3, 1) both;\n}\n\n.cb-think-pulse {\n  animation: cb-think-pulse 2.2s ease-in-out infinite;\n}\n\n.cb-bot-cursor,\n.cb-input-cursor {\n  animation: cb-blink 0.9s step-start infinite;\n}\n\n.cb-action-btn {\n  animation: cb-fade-up 0.4s cubic-bezier(0.16, 1, 0.3, 1) both;\n}\n\n.cb-action-btn:nth-child(1) {\n  animation-delay: 40ms;\n}\n.cb-action-btn:nth-child(2) {\n  animation-delay: 110ms;\n}\n.cb-action-btn:nth-child(3) {\n  animation-delay: 180ms;\n}\n.cb-action-btn:nth-child(4) {\n  animation-delay: 250ms;\n}\n.cb-action-btn:nth-child(5) {\n  animation-delay: 320ms;\n}\n\n.cb-chip {\n  animation: cb-chip-in 0.45s cubic-bezier(0.16, 1, 0.3, 1) both;\n}\n\n.cb-chip:nth-child(1) {\n  animation-delay: 500ms;\n}\n.cb-chip:nth-child(2) {\n  animation-delay: 640ms;\n}\n\n.cb-spinner {\n  animation: cb-spin 0.65s linear infinite;\n}\n\n.cb-send-btn-active {\n  animation: cb-send-pulse 0.3s cubic-bezier(0.22, 1, 0.36, 1);\n}\n\n@keyframes cb-bubble-in {\n  from {\n    opacity: 0;\n    transform: scale(0.91) translateY(8px);\n  }\n  to {\n    opacity: 1;\n    transform: scale(1) translateY(0);\n  }\n}\n\n@keyframes cb-chip-in {\n  from {\n    opacity: 0;\n    transform: translateX(-10px);\n  }\n  to {\n    opacity: 1;\n    transform: translateX(0);\n  }\n}\n\n@keyframes cb-fade-up {\n  from {\n    opacity: 0;\n    transform: translateY(10px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n@keyframes cb-blink {\n  0%,\n  45% {\n    opacity: 1;\n  }\n  55%,\n  100% {\n    opacity: 0;\n  }\n}\n\n@keyframes cb-think-pulse {\n  0%,\n  100% {\n    opacity: 0.3;\n  }\n  50% {\n    opacity: 1;\n  }\n}\n\n@keyframes cb-send-pulse {\n  0% {\n    transform: scale(1);\n  }\n  40% {\n    transform: scale(0.76);\n  }\n  100% {\n    transform: scale(1);\n  }\n}\n\n@keyframes cb-spin {\n  to {\n    transform: rotate(360deg);\n  }\n}\n",
      "type": "registry:file",
      "target": "app/chat-bot/styles/chat-bot.css"
    }
  ],
  "type": "registry:block"
}