{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "app-creator",
  "title": "AppCreator",
  "description": "The describe-an-app bar: auto-growing prompt, free/paid plan segment, and a create action with an in-flight state.",
  "dependencies": [
    "@hugeicons/react",
    "@hugeicons/core-free-icons"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.neon.com/r/neon-tokens.json",
    "https://ui.neon.com/r/button.json",
    "https://ui.neon.com/r/status-badge.json"
  ],
  "files": [
    {
      "path": "src/components/app-creator/app-creator.tsx",
      "content": "\"use client\";\n\nimport type { ComponentProps, KeyboardEvent } from \"react\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport type { AppPlan } from \"@/components/status-badge/status-badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\nexport type AppCreatorProps = Omit<ComponentProps<\"div\">, \"onSubmit\"> & {\n  /** Called with the prompt and plan when the user creates. */\n  onCreate: (prompt: string, plan: AppPlan) => void;\n  /** Show the in-flight state and lock the controls. */\n  isCreating?: boolean;\n  placeholder?: string;\n  /**\n   * Example prompts cycled through the placeholder with a typewriter\n   * rhythm while the field is empty. Overrides `placeholder`.\n   */\n  placeholderPrompts?: string[];\n  /** Uncontrolled initial plan. */\n  defaultPlan?: AppPlan;\n  /** Label for the create action. */\n  actionLabel?: string;\n  disabled?: boolean;\n};\n\n/* ─────────────────────────────────────────────────────────\n * The \"describe an app\" bar: one bordered surface whose\n * border warms neutrally while you type (color belongs to\n * the CTA and caret, never the frame). The prompt is an\n * auto-growing textarea; the foot holds a quiet free/paid\n * segment on the left and the create action on the right.\n * Enter creates (a quiet \"press ⏎\" hint appears once the\n * prompt is satisfied), Shift+Enter breaks a line. While\n * creating, everything locks and the CTA shimmers — the\n * prompt stays visible so the wait has context.\n * ───────────────────────────────────────────────────────── */\nconst PLANS: AppPlan[] = [\"free\", \"paid\"];\n\n/* ─────────────────────────────────────────────────────────\n * PLACEHOLDER STORYBOARD — \"the pitch\"\n *\n * While the field is empty, example prompts type themselves\n * out character by character, hold, then erase — the bar\n * keeps pitching ideas until you have your own. Typing a\n * single character stops the show instantly. Static first\n * prompt under reduced motion.\n * ───────────────────────────────────────────────────────── */\nconst TYPE_MS = 34;\nconst ERASE_MS = 12;\nconst HOLD_MS = 2200;\nconst BREATH_MS = 600;\nconst CARET = \"▍\";\n\n/** Cycles a typewriter placeholder through the given prompts. */\nconst useTypewriter = (prompts: string[] | undefined, active: boolean) => {\n  const [text, setText] = useState(prompts?.[0] ?? \"\");\n\n  useEffect(() => {\n    if (!(prompts?.length && active)) {\n      return;\n    }\n\n    const reduced = window.matchMedia(\n      \"(prefers-reduced-motion: reduce)\"\n    ).matches;\n\n    if (reduced) {\n      const timer = window.setTimeout(() => setText(prompts[0] ?? \"\"), 0);\n      return () => window.clearTimeout(timer);\n    }\n\n    let index = 0;\n    let length = 0;\n    let erasing = false;\n    let timer = 0;\n\n    const tick = () => {\n      const prompt = prompts[index % prompts.length] ?? \"\";\n\n      if (erasing) {\n        length -= 1;\n        setText(prompt.slice(0, length) + CARET);\n\n        if (length === 0) {\n          erasing = false;\n          index += 1;\n          // A breath of empty field before the next pitch begins.\n          timer = window.setTimeout(tick, BREATH_MS);\n          return;\n        }\n\n        timer = window.setTimeout(tick, ERASE_MS);\n        return;\n      }\n\n      length += 1;\n      setText(prompt.slice(0, length) + CARET);\n\n      if (length >= prompt.length) {\n        erasing = true;\n        timer = window.setTimeout(tick, HOLD_MS);\n        return;\n      }\n\n      timer = window.setTimeout(tick, TYPE_MS);\n    };\n\n    timer = window.setTimeout(tick, 400);\n    return () => window.clearTimeout(timer);\n  }, [prompts, active]);\n\n  return text;\n};\n\n/* ─────────────────────────────────────────────────────────\n * CTA STORYBOARD — \"ignition\"\n *\n *  empty      dormant ghost: hairline border, muted label\n *             with the same slow shimmer drifting across —\n *             the button breathes even before it's earned.\n *  satisfied  ignition: the button fills to neon on a\n *             300ms ramp, a soft primary glow blooms, and\n *             a slow shimmer sweep loops across the label —\n *             the moment the app becomes possible.\n *  hover      the glow leans brighter; color and shadow\n *             only, nothing moves.\n *  creating   \"Creating…\" under the continuous shimmer\n *             (the platform's working language); all\n *             motion stops under reduced motion.\n * ───────────────────────────────────────────────────────── */\n/* The charged CTA speaks through color and the sweep alone — no glow. */\nconst CTA_GLOW = \"\";\n\n/** The ignition CTA: ghost until earned, neon + sweep after. */\nconst CreateAction = ({\n  actionLabel,\n  isCreating,\n  locked,\n  onCreate,\n  state,\n}: {\n  actionLabel: string;\n  isCreating: boolean;\n  locked: boolean;\n  onCreate: () => void;\n  state: \"off\" | \"ready\" | \"creating\";\n}) => (\n  <Button\n    className={cn(\n      \"relative overflow-hidden transition-[background-color,color,border-color,box-shadow] duration-300\",\n      state === \"off\"\n        ? \"disabled:border-border/60 disabled:bg-transparent disabled:text-muted-foreground disabled:opacity-100\"\n        : cn(CTA_GLOW, \"disabled:opacity-100\")\n    )}\n    disabled={locked || state === \"off\"}\n    onClick={onCreate}\n    size=\"sm\"\n  >\n    <span className=\"shimmer shimmer-duration-2400 relative\" key={state}>\n      {isCreating ? \"Creating…\" : actionLabel}\n    </span>\n  </Button>\n);\n\nexport const AppCreator = ({\n  actionLabel = \"Create app\",\n  className,\n  defaultPlan = \"free\",\n  disabled = false,\n  isCreating = false,\n  onCreate,\n  placeholder = \"Describe the app you want to build…\",\n  placeholderPrompts,\n  ...props\n}: AppCreatorProps) => {\n  const [prompt, setPrompt] = useState(\"\");\n  const [plan, setPlan] = useState<AppPlan>(defaultPlan);\n  const textareaRef = useRef<HTMLTextAreaElement>(null);\n  const locked = disabled || isCreating;\n  const typed = useTypewriter(\n    placeholderPrompts,\n    prompt.length === 0 && !locked\n  );\n  const livePlaceholder = placeholderPrompts?.length ? typed : placeholder;\n\n  const autoGrow = useCallback(() => {\n    const textarea = textareaRef.current;\n\n    if (textarea) {\n      textarea.style.height = \"auto\";\n      textarea.style.height = `${Math.min(textarea.scrollHeight, 160)}px`;\n    }\n  }, []);\n\n  const create = () => {\n    const trimmed = prompt.trim();\n\n    if (trimmed.length > 0 && !locked) {\n      onCreate(trimmed, plan);\n    }\n  };\n\n  const ctaState = (): \"off\" | \"ready\" | \"creating\" => {\n    if (isCreating) {\n      return \"creating\";\n    }\n\n    return prompt.trim().length > 0 ? \"ready\" : \"off\";\n  };\n\n  const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {\n    if (event.key === \"Enter\" && !event.shiftKey) {\n      event.preventDefault();\n      create();\n    }\n  };\n\n  return (\n    <div\n      className={cn(\n        \"flex flex-col rounded-lg border border-border/60 bg-card transition-colors focus-within:border-border hover:border-border\",\n        isCreating && \"neon-submit-pulse\",\n        disabled && !isCreating && \"opacity-80\",\n        className\n      )}\n      data-creating={isCreating || undefined}\n      data-slot=\"app-creator\"\n      {...props}\n    >\n      <textarea\n        aria-label=\"Describe the app you want to build\"\n        className=\"max-h-40 min-h-16 w-full resize-none bg-transparent p-3 text-base caret-primary outline-none placeholder:text-muted-foreground/60 disabled:cursor-not-allowed sm:text-sm\"\n        disabled={locked}\n        onChange={(event) => {\n          setPrompt(event.target.value);\n          autoGrow();\n        }}\n        onKeyDown={handleKeyDown}\n        placeholder={livePlaceholder}\n        ref={textareaRef}\n        rows={2}\n        value={prompt}\n      />\n      <div className=\"flex items-center gap-1.5 border-border/40 border-t p-2\">\n        <fieldset\n          className=\"relative isolate grid grid-cols-2 rounded-sm border border-border/60 p-0.5\"\n          data-slot=\"app-creator-plans\"\n          disabled={locked}\n        >\n          <legend className=\"sr-only\">Plan</legend>\n          <span\n            aria-hidden=\"true\"\n            className={cn(\n              // Nested radius: container 4px minus the 2px gap.\n              \"-z-10 absolute inset-y-0.5 left-0.5 w-[calc(50%-2px)] rounded-[2px] transition-[translate,background-color,border-color] duration-300 ease-[cubic-bezier(0.34,1.3,0.64,1)] motion-reduce:transition-none\",\n              plan === \"paid\"\n                ? \"translate-x-full border border-primary/40 bg-primary/10\"\n                : \"border border-transparent bg-muted/40\"\n            )}\n            data-slot=\"app-creator-plan-thumb\"\n          />\n          {PLANS.map((option) => (\n            <label\n              className={cn(\n                \"cursor-pointer px-3 py-1 text-center font-mono text-xs transition-colors duration-200 focus-within:outline focus-within:outline-primary\",\n                locked && \"cursor-not-allowed\",\n                plan === option && option === \"paid\" && \"text-primary\",\n                plan === option && option === \"free\" && \"text-foreground\",\n                plan !== option && \"text-muted-foreground hover:text-foreground\"\n              )}\n              key={option}\n            >\n              <input\n                checked={plan === option}\n                className=\"sr-only\"\n                name=\"app-creator-plan\"\n                onChange={() => setPlan(option)}\n                type=\"radio\"\n                value={option}\n              />\n              {option}\n            </label>\n          ))}\n        </fieldset>\n        <span\n          aria-hidden=\"true\"\n          className={cn(\n            \"ml-auto font-mono text-[10px] text-muted-foreground/60 transition-opacity duration-300\",\n            ctaState() === \"ready\" ? \"opacity-100\" : \"opacity-0\"\n          )}\n        >\n          press ⏎\n        </span>\n        <CreateAction\n          actionLabel={actionLabel}\n          isCreating={isCreating}\n          locked={locked}\n          onCreate={create}\n          state={ctaState()}\n        />\n      </div>\n    </div>\n  );\n};\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}