{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pied-piper-onboarding",
  "title": "Pied Piper Onboarding",
  "description": "An interactive sign-up flow — Pied Piper header, typed assistant lines, thinking between turns, and the Claude composer for name + intent.",
  "registryDependencies": [
    "https://brainless.swerdlow.dev/r/claude-message.json",
    "https://brainless.swerdlow.dev/r/claude-thinking.json",
    "https://brainless.swerdlow.dev/r/claude-prompt.json",
    "https://brainless.swerdlow.dev/r/claude-todo-list.json"
  ],
  "files": [
    {
      "path": "registry/brainless/blocks/pied-piper-onboarding.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { ClaudeMessage } from \"@/registry/brainless/claude/claude-message\";\nimport { ClaudeThinking } from \"@/registry/brainless/claude/claude-thinking\";\nimport { ClaudePrompt } from \"@/registry/brainless/claude/claude-prompt\";\nimport { ClaudeTodoList, type Todo } from \"@/registry/brainless/claude/claude-todo-list\";\n\n/**\n * PiedPiperOnboarding — an interactive sign-up flow built from brainless\n * components. A Claude-style header with a terminal Pied Piper logo, typed\n * assistant lines, thinking between turns, and the real ❯ composer for input.\n */\n\nconst GREEN = \"#39b54a\";\nconst GREEN_DIM = \"#2a8a38\";\nconst GRAY = \"#949494\";\nconst FG = \"#c0caf5\";\n\n// Side-view flute / pipe — Silicon Valley Pied Piper, as a 1-bit terminal sprite.\nconst LOGO_BITS = [\n  \"000000001111000000\",\n  \"000000111111110000\",\n  \"000011111111111100\",\n  \"001111010101011110\",\n  \"011111111111111111\",\n  \"001111010101011110\",\n  \"000011111111111100\",\n  \"000000111111110000\",\n  \"000000001111000000\",\n];\n\nfunction PiedPiperLogo({\n  scale = 3.5,\n  color = GREEN,\n  className,\n}: {\n  scale?: number;\n  color?: string;\n  className?: string;\n}) {\n  const w = LOGO_BITS[0].length;\n  const h = LOGO_BITS.length;\n  const PH = 2.2;\n  const rects: React.ReactElement[] = [];\n  LOGO_BITS.forEach((row, y) => {\n    let x = 0;\n    while (x < w) {\n      if (row[x] === \"1\") {\n        let end = x;\n        while (end < w && row[end] === \"1\") end += 1;\n        rects.push(\n          <rect key={`${x}-${y}`} x={x} y={y * PH} width={end - x} height={PH} />,\n        );\n        x = end;\n      } else {\n        x += 1;\n      }\n    }\n  });\n  return (\n    <svg\n      aria-hidden\n      width={w * scale}\n      height={h * PH * scale}\n      viewBox={`0 0 ${w} ${h * PH}`}\n      shapeRendering=\"crispEdges\"\n      fill={color}\n      className={className}\n    >\n      {rects}\n    </svg>\n  );\n}\n\nfunction PiedPiperHeader({ className }: { className?: string }) {\n  return (\n    <fieldset\n      className={cn(\n        \"rounded-[6px] border px-4 pb-3.5 pt-1 font-mono text-[13px] leading-[1.5]\",\n        className,\n      )}\n      style={{ borderColor: GREEN, color: FG }}\n    >\n      <legend className=\"px-2\" style={{ color: GREEN }}>\n        Pied Piper <span style={{ color: GRAY }}>v3.1.4</span>\n      </legend>\n\n      <div className=\"grid gap-4 sm:grid-cols-[1fr_1px_1.1fr]\">\n        <div className=\"flex flex-col items-center gap-2 py-1 text-center\">\n          <div className=\"font-semibold\">Welcome to Pied Piper</div>\n          <PiedPiperLogo className=\"my-1.5\" />\n          <div className=\"space-y-0.5\" style={{ color: GRAY }}>\n            <div>compression · middle-out</div>\n            <div>onboarding · new account</div>\n          </div>\n        </div>\n\n        <div\n          aria-hidden\n          className=\"hidden sm:block\"\n          style={{ background: `${GREEN}55` }}\n        />\n\n        <div className=\"min-w-0 space-y-1\">\n          <div className=\"font-semibold\" style={{ color: GREEN }}>\n            Tips for getting started\n          </div>\n          <div className=\"truncate\">Tell us your name</div>\n          <div className=\"truncate\">Say what you&apos;re looking for</div>\n          <div className=\"my-1.5 h-px\" style={{ background: GREEN }} />\n          <div className=\"font-semibold\" style={{ color: GREEN }}>\n            What&apos;s new\n          </div>\n          <div className=\"truncate\">Middle-out compression is live</div>\n          <div className=\"truncate\">Sign-up now fits in one turn</div>\n          <div className=\"truncate italic\" style={{ color: GRAY }}>\n            /release-notes for more\n          </div>\n        </div>\n      </div>\n    </fieldset>\n  );\n}\n\ntype Phase =\n  | \"greeting\"\n  | \"await-name\"\n  | \"think-name\"\n  | \"ask-looking\"\n  | \"await-looking\"\n  | \"think-looking\"\n  | \"done\"\n  | \"complete\";\n\ntype ChatLine =\n  | { kind: \"assistant\"; text: string; typing?: boolean }\n  | { kind: \"user\"; text: string };\n\nfunction useTypewriter(\n  text: string,\n  active: boolean,\n  ms = 22,\n): { displayed: string; done: boolean } {\n  const [displayed, setDisplayed] = React.useState(\"\");\n  const prefersReduced = usePrefersReducedMotion();\n\n  React.useEffect(() => {\n    if (!active) return;\n    if (prefersReduced) {\n      setDisplayed(text);\n      return;\n    }\n    setDisplayed(\"\");\n    let i = 0;\n    const id = setInterval(() => {\n      i += 1;\n      setDisplayed(text.slice(0, i));\n      if (i >= text.length) clearInterval(id);\n    }, ms);\n    return () => clearInterval(id);\n  }, [text, active, ms, prefersReduced]);\n\n  const done = active && displayed.length >= text.length;\n  return { displayed, done };\n}\n\nfunction usePrefersReducedMotion() {\n  const [reduced, setReduced] = React.useState(false);\n  React.useEffect(() => {\n    const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    setReduced(mq.matches);\n    const on = () => setReduced(mq.matches);\n    mq.addEventListener(\"change\", on);\n    return () => mq.removeEventListener(\"change\", on);\n  }, []);\n  return reduced;\n}\n\nfunction todosFor(phase: Phase, name: string): Todo[] {\n  const named = name.trim() || \"your name\";\n  const askName: Todo[\"status\"] =\n    phase === \"greeting\" || phase === \"await-name\"\n      ? \"active\"\n      : \"done\";\n  const askLooking: Todo[\"status\"] =\n    phase === \"ask-looking\" || phase === \"await-looking\" || phase === \"think-name\"\n      ? \"active\"\n      : phase === \"greeting\" || phase === \"await-name\"\n        ? \"todo\"\n        : \"done\";\n  const finish: Todo[\"status\"] =\n    phase === \"think-looking\" || phase === \"done\"\n      ? \"active\"\n      : phase === \"complete\"\n        ? \"done\"\n        : \"todo\";\n\n  return [\n    { label: `Collect ${named === \"your name\" ? \"a name\" : named}`, status: askName },\n    { label: \"Learn what they're looking for\", status: askLooking },\n    { label: \"Create the account\", status: finish },\n  ];\n}\n\n/**\n * PiedPiperOnboarding — interactive demo block.\n */\nexport function PiedPiperOnboarding() {\n  const [phase, setPhase] = React.useState<Phase>(\"greeting\");\n  const [lines, setLines] = React.useState<ChatLine[]>([]);\n  const [name, setName] = React.useState(\"\");\n  const [lookingFor, setLookingFor] = React.useState(\"\");\n  const [draft, setDraft] = React.useState(\"\");\n  const rootRef = React.useRef<HTMLDivElement>(null);\n\n  const greeting = \"Hey — what's your name?\";\n  const followUp = React.useMemo(() => {\n    const first = name.trim().split(/\\s+/)[0] || \"friend\";\n    return `Nice to meet you, ${first}. And what are you looking for?`;\n  }, [name]);\n  const finale = React.useMemo(() => {\n    const first = name.trim().split(/\\s+/)[0] || \"friend\";\n    const want = lookingFor.trim() || \"something great\";\n    return `You're in, ${first}. We've got you down for \"${want}\". Welcome to Pied Piper — compression so good, it feels illegal.`;\n  }, [name, lookingFor]);\n\n  const typingText =\n    phase === \"greeting\"\n      ? greeting\n      : phase === \"ask-looking\"\n        ? followUp\n        : phase === \"done\"\n          ? finale\n          : \"\";\n\n  const { displayed, done: typed } = useTypewriter(\n    typingText,\n    phase === \"greeting\" || phase === \"ask-looking\" || phase === \"done\",\n  );\n\n  // Commit typed lines into the transcript, then wait for input / finish.\n  React.useEffect(() => {\n    if (!typed) return;\n\n    const commit = (text: string, next: Phase) => {\n      setLines((prev) => {\n        const last = prev[prev.length - 1];\n        if (last?.kind === \"assistant\" && last.text === text) return prev;\n        return [...prev, { kind: \"assistant\", text }];\n      });\n      setPhase(next);\n    };\n\n    if (phase === \"greeting\") commit(greeting, \"await-name\");\n    else if (phase === \"ask-looking\") commit(followUp, \"await-looking\");\n    else if (phase === \"done\") commit(finale, \"complete\");\n  }, [typed, phase, greeting, followUp, finale]);\n\n  // Thinking delays.\n  React.useEffect(() => {\n    if (phase !== \"think-name\" && phase !== \"think-looking\") return;\n    const ms = 1400 + Math.random() * 600;\n    const id = setTimeout(() => {\n      if (phase === \"think-name\") setPhase(\"ask-looking\");\n      else setPhase(\"done\");\n    }, ms);\n    return () => clearTimeout(id);\n  }, [phase]);\n\n  // Focus the composer when waiting for input — never scroll the page.\n  React.useEffect(() => {\n    if (phase !== \"await-name\" && phase !== \"await-looking\" && phase !== \"complete\") {\n      return;\n    }\n    const input = rootRef.current?.querySelector<HTMLInputElement>(\n      'input[aria-label=\"Prompt\"]',\n    );\n    input?.focus({ preventScroll: true });\n  }, [phase]);\n\n  function submit() {\n    const value = draft.trim();\n    if (!value && phase !== \"complete\") return;\n\n    if (phase === \"await-name\") {\n      setName(value);\n      setLines((prev) => [...prev, { kind: \"user\", text: value }]);\n      setDraft(\"\");\n      setPhase(\"think-name\");\n      return;\n    }\n\n    if (phase === \"await-looking\") {\n      setLookingFor(value);\n      setLines((prev) => [...prev, { kind: \"user\", text: value }]);\n      setDraft(\"\");\n      setPhase(\"think-looking\");\n      return;\n    }\n\n    if (phase === \"complete\") {\n      setPhase(\"greeting\");\n      setLines([]);\n      setName(\"\");\n      setLookingFor(\"\");\n      setDraft(\"\");\n    }\n  }\n\n  const waiting =\n    phase === \"await-name\" || phase === \"await-looking\" || phase === \"complete\";\n  const thinking = phase === \"think-name\" || phase === \"think-looking\";\n  const showTyping =\n    phase === \"greeting\" || phase === \"ask-looking\" || phase === \"done\";\n\n  const placeholder =\n    phase === \"await-name\"\n      ? \"your name\"\n      : phase === \"await-looking\"\n        ? \"e.g. lossless video compression\"\n        : phase === \"complete\"\n          ? \"press enter to restart\"\n          : \"\";\n\n  return (\n    <div\n      ref={rootRef}\n      className=\"space-y-3 font-mono text-[13px] leading-[1.6]\"\n      style={{ color: FG }}\n    >\n      <PiedPiperHeader />\n\n      <ClaudeTodoList todos={todosFor(phase, name)} />\n\n      <div className=\"space-y-3 pt-1\">\n        {lines.map((line, i) =>\n          line.kind === \"user\" ? (\n            <ClaudeMessage key={i} role=\"user\">\n              {line.text}\n            </ClaudeMessage>\n          ) : (\n            <ClaudeMessage key={i}>{line.text}</ClaudeMessage>\n          ),\n        )}\n\n        {showTyping ? (\n          <ClaudeMessage>\n            {displayed}\n            {!typed ? (\n              <span\n                aria-hidden\n                className=\"ml-0.5 inline-block w-[0.55ch] animate-pulse\"\n                style={{\n                  background: GREEN,\n                  height: \"1.1em\",\n                  verticalAlign: \"text-bottom\",\n                }}\n              />\n            ) : null}\n          </ClaudeMessage>\n        ) : null}\n\n        {thinking ? (\n          <ClaudeThinking\n            verbs={\n              phase === \"think-name\"\n                ? [\"Remembering\", \"Indexing\", \"Noodling\"]\n                : [\"Compressing\", \"Provisioning\", \"Conjuring\"]\n            }\n            showTokens={false}\n          />\n        ) : null}\n      </div>\n\n      <div className=\"pt-2\">\n        <ClaudePrompt\n          value={draft}\n          onChange={(e) => setDraft(e.target.value)}\n          onKeyDown={(e) => {\n            if (e.key === \"Enter\") {\n              e.preventDefault();\n              if (waiting) submit();\n            }\n          }}\n          placeholder={placeholder}\n          mode=\"auto\"\n          effort={false}\n          className={cn(!waiting && \"pointer-events-none opacity-50\")}\n        />\n      </div>\n\n      {phase === \"complete\" ? (\n        <div className=\"pt-1 text-[12px]\" style={{ color: GREEN_DIM }}>\n          ✓ account created · press enter to run it again\n        </div>\n      ) : null}\n    </div>\n  );\n}\n",
      "type": "registry:block",
      "target": "components/brainless/blocks/pied-piper-onboarding.tsx"
    }
  ],
  "type": "registry:block"
}