{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "simple-chatbot",
  "title": "Simple Chatbot",
  "description": "Prompt card with rotating SaaS-style placeholders, light/dark via .dark ancestor, shadcn Button toolbar (add + voice).",
  "registryDependencies": [
    "button"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/simple-chatbot/page.tsx",
      "content": "import SimpleChatbot from \"./components/simple-chatbot\"\n\nexport default function Page() {\n  return (\n    <div className=\"px-4 py-10 md:px-6 md:py-14\">\n      <SimpleChatbot />\n    </div>\n  )\n}\n",
      "type": "registry:page",
      "target": "app/simple-chatbot/page.tsx"
    },
    {
      "path": "registry/new-york/blocks/simple-chatbot/components/simple-chatbot.tsx",
      "content": "\"use client\"\n\nimport { cn } from \"@/lib/utils\"\n\nimport PromptInput from \"./prompt-input\"\nimport { PLACEHOLDER_PROMPTS } from \"../lib/config\"\nimport { usePlaceholderLoop } from \"../hooks/use-placeholder-loop\"\nimport \"../styles/simple-chatbot.css\"\n\ntype SimpleChatbotProps = {\n  className?: string\n  prompts?: readonly string[]\n}\n\nexport default function SimpleChatbot({\n  className,\n  prompts = PLACEHOLDER_PROMPTS,\n}: SimpleChatbotProps) {\n  const { displayText, showCursor, caretSolid } = usePlaceholderLoop(prompts)\n\n  return (\n    <div\n      className={cn(\n        \"scb-root scb-page mx-auto flex w-full max-w-3xl min-w-0 justify-center px-3.5 py-[18px]\",\n        className\n      )}\n    >\n      <div className=\"w-full max-w-[720px]\">\n        <PromptInput\n          displayText={displayText}\n          showCursor={showCursor}\n          caretSolid={caretSolid}\n        />\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "app/simple-chatbot/components/simple-chatbot.tsx"
    },
    {
      "path": "registry/new-york/blocks/simple-chatbot/components/prompt-input.tsx",
      "content": "import { Mic, Plus } from \"lucide-react\"\n\nimport { Button } from \"@/components/ui/button\"\nimport { cn } from \"@/lib/utils\"\n\ninterface PromptInputProps {\n  displayText: string\n  showCursor: boolean\n  caretSolid: boolean\n}\n\nconst iconProps = { strokeWidth: 1.5 } as const\n\nexport default function PromptInput({\n  displayText,\n  showCursor,\n  caretSolid,\n}: PromptInputProps) {\n  return (\n    <div className=\"overflow-hidden rounded-xl border border-border bg-muted shadow-[0_0_0_1px_rgba(0,0,0,0.06)] dark:bg-background dark:shadow-[0_0_0_1px_rgba(255,255,255,0.03)]\">\n      <div className=\"min-h-[76px] px-3.5 pt-3.5 pb-2\">\n        <div\n          className=\"scb-input flex min-h-10 flex-wrap items-start text-sm leading-normal\"\n          aria-hidden={!displayText && !showCursor}\n        >\n          {displayText ? (\n            <span className=\"scb-text text-foreground blur-[0.22px] select-none\">\n              {displayText}\n            </span>\n          ) : null}\n          {showCursor ? (\n            <span\n              className={cn(\n                \"scb-caret ml-0.5 inline-block shrink-0 translate-y-[0.06em] align-text-top text-[1.22em] leading-none font-medium text-foreground\",\n                caretSolid && \"scb-caret--solid\"\n              )}\n              aria-hidden\n            >\n              |\n            </span>\n          ) : null}\n        </div>\n      </div>\n\n      <div className=\"flex items-center justify-between gap-2 px-2.5 pb-2.5 pl-3\">\n        <div className=\"flex min-w-0 flex-wrap items-center gap-1\">\n          <Button\n            type=\"button\"\n            variant=\"ghost\"\n            size=\"icon-sm\"\n            className=\"h-[30px] w-[30px] min-w-[30px] rounded-lg text-muted-foreground hover:bg-foreground/6 hover:text-foreground\"\n            aria-label=\"Add context\"\n          >\n            <Plus size={17} {...iconProps} />\n          </Button>\n        </div>\n\n        <div className=\"flex min-w-0 items-center gap-1\">\n          <Button\n            type=\"button\"\n            variant=\"ghost\"\n            className=\"h-[34px] w-[34px] min-w-[34px] shrink-0 rounded-[10px] bg-foreground p-0 text-background hover:bg-foreground hover:opacity-95\"\n            aria-label=\"Voice input\"\n          >\n            <Mic size={17} className=\"text-background\" />\n          </Button>\n        </div>\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "app/simple-chatbot/components/prompt-input.tsx"
    },
    {
      "path": "registry/new-york/blocks/simple-chatbot/hooks/use-placeholder-loop.ts",
      "content": "import { useLayoutEffect, useReducer } from \"react\"\n\nimport {\n  BETWEEN_MS,\n  DELETE_CHAR_MS_FAST,\n  DELETE_CHAR_MS_SLOW,\n  HOLD_MS,\n  PLACEHOLDER_PROMPTS,\n  TYPE_CHAR_MS_FAST,\n  TYPE_CHAR_MS_SLOW,\n} from \"../lib/config\"\n\nfunction typeDelayMs(charIdx: number, len: number): number {\n  if (len <= 0) return TYPE_CHAR_MS_SLOW\n  const t = charIdx / len\n  // Gentle ease — smoother tempo than a steep power curve\n  const ease = Math.pow(1 - t, 0.28)\n  return Math.round(\n    TYPE_CHAR_MS_FAST + (TYPE_CHAR_MS_SLOW - TYPE_CHAR_MS_FAST) * ease\n  )\n}\n\nfunction deleteDelayMs(charIdx: number, len: number): number {\n  if (len <= 0) return DELETE_CHAR_MS_SLOW\n  const t = charIdx / len\n  const ease = Math.pow(t, 0.38)\n  return Math.round(\n    DELETE_CHAR_MS_FAST + (DELETE_CHAR_MS_SLOW - DELETE_CHAR_MS_FAST) * ease\n  )\n}\n\ntype Phase = \"typingIn\" | \"hold\" | \"typingOut\" | \"between\"\n\ntype State = {\n  phase: Phase\n  promptIdx: number\n  charIdx: number\n}\n\ntype Action =\n  | { type: \"ADVANCE_CHAR\" }\n  | { type: \"SET_PHASE\"; phase: Phase; charIdx?: number; promptIdx?: number }\n  | { type: \"REWIND_CHAR\" }\n\nconst INITIAL: State = { phase: \"typingIn\", promptIdx: 0, charIdx: 0 }\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 \"REWIND_CHAR\":\n      return { ...state, charIdx: Math.max(0, state.charIdx - 1) }\n    case \"SET_PHASE\":\n      return {\n        phase: action.phase,\n        charIdx: action.charIdx ?? state.charIdx,\n        promptIdx: action.promptIdx ?? state.promptIdx,\n      }\n    default:\n      return state\n  }\n}\n\nexport function usePlaceholderLoop(prompts: readonly string[] = PLACEHOLDER_PROMPTS) {\n  const [state, dispatch] = useReducer(reducer, INITIAL)\n  const { phase, promptIdx, charIdx } = state\n\n  const text = prompts[promptIdx] ?? \"\"\n  const len = text.length\n\n  const displayText = text.slice(0, charIdx)\n  const showCursor =\n    phase === \"typingIn\" || phase === \"typingOut\" || phase === \"between\" || phase === \"hold\"\n\n  const caretSolid = phase === \"typingIn\" || phase === \"typingOut\"\n\n  useLayoutEffect(() => {\n    let t: ReturnType<typeof setTimeout>\n\n    switch (phase) {\n      case \"typingIn\":\n        if (charIdx < len) {\n          t = setTimeout(() => dispatch({ type: \"ADVANCE_CHAR\" }), typeDelayMs(charIdx, len))\n        } else {\n          dispatch({ type: \"SET_PHASE\", phase: \"hold\" })\n        }\n        break\n\n      case \"hold\":\n        t = setTimeout(() => dispatch({ type: \"SET_PHASE\", phase: \"typingOut\" }), HOLD_MS)\n        break\n\n      case \"typingOut\":\n        if (charIdx > 0) {\n          t = setTimeout(\n            () => dispatch({ type: \"REWIND_CHAR\" }),\n            deleteDelayMs(charIdx, len)\n          )\n        } else {\n          dispatch({ type: \"SET_PHASE\", phase: \"between\", charIdx: 0 })\n        }\n        break\n\n      case \"between\":\n        t = setTimeout(() => {\n          const next = (promptIdx + 1) % prompts.length\n          dispatch({\n            type: \"SET_PHASE\",\n            phase: \"typingIn\",\n            charIdx: 0,\n            promptIdx: next,\n          })\n        }, BETWEEN_MS)\n        break\n    }\n\n    return () => clearTimeout(t)\n  }, [phase, charIdx, len, promptIdx, prompts.length])\n\n  return { displayText, showCursor, caretSolid }\n}\n",
      "type": "registry:hook",
      "target": "app/simple-chatbot/hooks/use-placeholder-loop.ts"
    },
    {
      "path": "registry/new-york/blocks/simple-chatbot/lib/config.ts",
      "content": "/** Type speed eased between these bounds (ms); narrow span + gentle curve = snappy, even cadence */\nexport const TYPE_CHAR_MS_SLOW = 40\nexport const TYPE_CHAR_MS_FAST = 18\n\n/** Delete eased between these bounds (ms); quick bulk with a soft lead-in */\nexport const DELETE_CHAR_MS_SLOW = 30\nexport const DELETE_CHAR_MS_FAST = 11\n\n/** How long the full placeholder stays visible before deleting */\nexport const HOLD_MS = 1500\n\n/** Brief pause after clearing before the next phrase */\nexport const BETWEEN_MS = 280\n\nexport const PLACEHOLDER_PROMPTS = [\n  \"Explain the concept of quantum entanglement.\",\n  \"Why does time slow down near a black hole?\",\n  \"What is the Fermi Paradox and why does it matter?\",\n  \"How did the universe begin before the Big Bang?\",\n  \"What would happen if you fell into a black hole?\",\n  \"Is mathematics discovered or invented by humans?\",\n] as const\n",
      "type": "registry:lib",
      "target": "app/simple-chatbot/lib/config.ts"
    },
    {
      "path": "registry/new-york/blocks/simple-chatbot/styles/simple-chatbot.css",
      "content": "/* simple-chatbot — caret blink */\n\n@keyframes scb-caret-blink {\n  0%,\n  45% {\n    opacity: 1;\n  }\n  55%,\n  100% {\n    opacity: 0;\n  }\n}\n\n.scb-caret {\n  animation: scb-caret-blink 0.92s step-start infinite;\n}\n\n.scb-caret--solid {\n  animation: none;\n  opacity: 1;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .scb-caret {\n    animation: none;\n  }\n}\n",
      "type": "registry:file",
      "target": "app/simple-chatbot/styles/simple-chatbot.css"
    }
  ],
  "type": "registry:block"
}