{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "contact-form",
  "title": "Contact Form",
  "description": "Multi-step contact form with editable CONTENT; posts to your API.",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "files": [
    {
      "path": "registry/default/blocks/contact-form.tsx",
      "content": "\"use client\"\n\nimport { ArrowLeft, ArrowRight, Check, Loader2, Send } from \"lucide-react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { useState, type FormEvent } from \"react\"\n\n/**\n * Edit CONTENT / PROJECT_TYPES to match your studio intake.\n * Posts JSON to CONTENT.endpoint (wire your own API route).\n * Steps reveal one at a time (Family: gradual revelation + fluid travel).\n */\nconst CONTENT = {\n  stamp: \"Contact\",\n  headline: \"Tell us what you want to ship.\",\n  lede: \"Four short steps. We reply within one business day.\",\n  endpoint: \"/api/contact\",\n  successTitle: \"Got it.\",\n  successBody: \"We'll reply within one business day.\",\n  submitLabel: \"Send\",\n}\n\nconst PROJECT_TYPES = [\n  { id: \"mvp-sprint\", label: \"7-Day MVP Sprint\", hint: \"from $4,800\" },\n  { id: \"ai-integration\", label: \"AI Feature\", hint: \"from $2,400\" },\n  { id: \"design-system\", label: \"Design System\", hint: \"from $3,600\" },\n  { id: \"other\", label: \"Something else\", hint: \"Describe it below\" },\n] as const\n\nconst BUDGETS = [\n  { id: \"<2k\", label: \"Under $2k\" },\n  { id: \"2k-5k\", label: \"$2k - $5k\" },\n  { id: \"5k-10k\", label: \"$5k - $10k\" },\n  { id: \"10k+\", label: \"$10k+\" },\n  { id: \"unsure\", label: \"Not sure yet\" },\n] as const\n\nconst TIMELINES = [\n  { id: \"asap\", label: \"ASAP · 1-2 weeks\" },\n  { id: \"1-month\", label: \"Within a month\" },\n  { id: \"flexible\", label: \"Flexible\" },\n  { id: \"unsure\", label: \"Not sure yet\" },\n] as const\n\nconst STEPS = [\n  { id: \"who\", label: \"Who\", title: \"Who should we reply to?\" },\n  { id: \"what\", label: \"What\", title: \"What are we scoping?\" },\n  { id: \"when\", label: \"When\", title: \"Timing & budget\" },\n  { id: \"send\", label: \"Send\", title: \"Look right?\" },\n] as const\n\ntype Status =\n  | { kind: \"idle\" }\n  | { kind: \"submitting\" }\n  | { kind: \"success\" }\n  | { kind: \"error\"; message: string }\n\nfunction Chip({\n  pressed,\n  onClick,\n  disabled,\n  children,\n}: {\n  pressed: boolean\n  onClick: () => void\n  disabled?: boolean\n  children: React.ReactNode\n}) {\n  return (\n    <button\n      type=\"button\"\n      onClick={onClick}\n      aria-pressed={pressed}\n      disabled={disabled}\n      className={\n        pressed\n          ? \"border border-[var(--color-brand,#0b7bff)] bg-[var(--color-brand,#0b7bff)]/10 px-3.5 py-2.5 text-left text-sm text-foreground\"\n          : \"border border-border-subtle bg-background px-3.5 py-2.5 text-left text-sm text-muted-foreground hover:border-border hover:text-foreground\"\n      }\n    >\n      {children}\n    </button>\n  )\n}\n\nexport function ContactForm() {\n  const reduce = useReducedMotion()\n  const [step, setStep] = useState(0)\n  const [status, setStatus] = useState<Status>({ kind: \"idle\" })\n  const [form, setForm] = useState({\n    name: \"\",\n    email: \"\",\n    company: \"\",\n    projectType: \"\",\n    budget: \"\",\n    timeline: \"\",\n    message: \"\",\n    honeypot: \"\",\n  })\n\n  const canNext = () => {\n    if (step === 0) return !!form.name.trim() && !!form.email.trim()\n    if (step === 1) return !!form.message.trim()\n    return true\n  }\n\n  const onSubmit = async (e: FormEvent) => {\n    e.preventDefault()\n    if (step < STEPS.length - 1) {\n      if (!canNext()) return\n      setStep((s) => s + 1)\n      return\n    }\n\n    setStatus({ kind: \"submitting\" })\n    try {\n      const res = await fetch(CONTENT.endpoint, {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify(form),\n      })\n      if (!res.ok) {\n        const data = (await res.json().catch(() => null)) as {\n          error?: string\n        } | null\n        throw new Error(data?.error ?? \"Something went wrong. Try again.\")\n      }\n      setStatus({ kind: \"success\" })\n    } catch (err) {\n      setStatus({\n        kind: \"error\",\n        message: err instanceof Error ? err.message : \"Request failed.\",\n      })\n    }\n  }\n\n  if (status.kind === \"success\") {\n    return (\n      <motion.section\n        className=\"border border-border-subtle p-6 sm:p-8\"\n        initial={{ opacity: 0, y: 8 }}\n        animate={{ opacity: 1, y: 0 }}\n        transition={{ duration: 0.28, ease: [0.32, 0.72, 0, 1] }}\n      >\n        <div className=\"flex size-10 items-center justify-center rounded-full bg-[var(--color-brand,#0b7bff)]/15 text-[var(--color-brand,#0b7bff)]\">\n          <Check className=\"size-5\" aria-hidden />\n        </div>\n        <h2 className=\"mt-4 text-2xl font-medium text-foreground\">\n          {CONTENT.successTitle}\n        </h2>\n        <p className=\"mt-2 text-sm text-muted-foreground\">{CONTENT.successBody}</p>\n      </motion.section>\n    )\n  }\n\n  const current = STEPS[step]!\n\n  return (\n    <section className=\"border border-border-subtle\">\n      <div className=\"border-b border-border-subtle px-6 py-6 sm:px-8\">\n        <p className=\"text-[11px] font-semibold tracking-[0.14em] text-muted-foreground uppercase\">\n          {CONTENT.stamp}\n        </p>\n        <h2 className=\"mt-3 text-2xl font-medium tracking-tight text-foreground sm:text-3xl\">\n          {CONTENT.headline}\n        </h2>\n        <p className=\"mt-2 text-sm text-muted-foreground\">{CONTENT.lede}</p>\n        <ol className=\"mt-6 flex flex-wrap gap-2\" aria-label=\"Form progress\">\n          {STEPS.map((s, i) => (\n            <li\n              key={s.id}\n              className={\n                i === step\n                  ? \"text-xs font-medium text-foreground\"\n                  : i < step\n                    ? \"text-xs text-[var(--color-brand,#0b7bff)]\"\n                    : \"text-xs text-muted-foreground\"\n              }\n            >\n              {String(i + 1).padStart(2, \"0\")} {s.label}\n              {i < STEPS.length - 1 ? (\n                <span className=\"mx-2 text-border-subtle\" aria-hidden>\n                  /\n                </span>\n              ) : null}\n            </li>\n          ))}\n        </ol>\n      </div>\n\n      <form onSubmit={onSubmit} className=\"space-y-6 p-6 sm:p-8\">\n        <input\n          type=\"text\"\n          name=\"company_website\"\n          value={form.honeypot}\n          onChange={(e) => setForm((f) => ({ ...f, honeypot: e.target.value }))}\n          className=\"hidden\"\n          tabIndex={-1}\n          autoComplete=\"off\"\n          aria-hidden\n        />\n\n        <AnimatePresence mode=\"wait\" initial={false}>\n          <motion.div\n            key={current.id}\n            initial={reduce ? false : { opacity: 0, y: 10 }}\n            animate={{ opacity: 1, y: 0 }}\n            exit={reduce ? undefined : { opacity: 0, y: -8 }}\n            transition={{ duration: 0.22, ease: [0.32, 0.72, 0, 1] }}\n            className=\"space-y-6\"\n            style={{ minHeight: 160 + step * 24 }}\n          >\n            <div>\n              <h3 className=\"text-lg font-medium text-foreground\">\n                {current.title}\n              </h3>\n            </div>\n\n            {step === 0 ? (\n          <div className=\"grid gap-4 sm:grid-cols-2\">\n            <label className=\"block space-y-1.5 sm:col-span-1\">\n              <span className=\"text-xs font-medium text-muted-foreground\">\n                Name\n              </span>\n              <input\n                required\n                value={form.name}\n                onChange={(e) =>\n                  setForm((f) => ({ ...f, name: e.target.value }))\n                }\n                className=\"h-11 w-full border border-border-subtle bg-background px-3 text-sm outline-none focus:border-foreground\"\n              />\n            </label>\n            <label className=\"block space-y-1.5 sm:col-span-1\">\n              <span className=\"text-xs font-medium text-muted-foreground\">\n                Email\n              </span>\n              <input\n                required\n                type=\"email\"\n                value={form.email}\n                onChange={(e) =>\n                  setForm((f) => ({ ...f, email: e.target.value }))\n                }\n                className=\"h-11 w-full border border-border-subtle bg-background px-3 text-sm outline-none focus:border-foreground\"\n              />\n            </label>\n            <label className=\"block space-y-1.5 sm:col-span-2\">\n              <span className=\"text-xs font-medium text-muted-foreground\">\n                Company (optional)\n              </span>\n              <input\n                value={form.company}\n                onChange={(e) =>\n                  setForm((f) => ({ ...f, company: e.target.value }))\n                }\n                className=\"h-11 w-full border border-border-subtle bg-background px-3 text-sm outline-none focus:border-foreground\"\n              />\n            </label>\n          </div>\n            ) : null}\n\n            {step === 1 ? (\n          <div className=\"space-y-4\">\n            <div>\n              <p className=\"mb-2 text-xs font-medium text-muted-foreground\">\n                Project type\n              </p>\n              <div className=\"grid gap-2 sm:grid-cols-2\">\n                {PROJECT_TYPES.map((p) => (\n                  <Chip\n                    key={p.id}\n                    pressed={form.projectType === p.id}\n                    onClick={() =>\n                      setForm((f) => ({ ...f, projectType: p.id }))\n                    }\n                  >\n                    <span className=\"block font-medium text-foreground\">\n                      {p.label}\n                    </span>\n                    <span className=\"mt-0.5 block text-xs text-muted-foreground\">\n                      {p.hint}\n                    </span>\n                  </Chip>\n                ))}\n              </div>\n            </div>\n            <label className=\"block space-y-1.5\">\n              <span className=\"text-xs font-medium text-muted-foreground\">\n                What do you want to ship?\n              </span>\n              <textarea\n                required\n                rows={4}\n                value={form.message}\n                onChange={(e) =>\n                  setForm((f) => ({ ...f, message: e.target.value }))\n                }\n                className=\"w-full border border-border-subtle bg-background px-3 py-2.5 text-sm outline-none focus:border-foreground\"\n              />\n            </label>\n          </div>\n            ) : null}\n\n            {step === 2 ? (\n          <div className=\"space-y-4\">\n            <div>\n              <p className=\"mb-2 text-xs font-medium text-muted-foreground\">\n                Budget\n              </p>\n              <div className=\"flex flex-wrap gap-2\">\n                {BUDGETS.map((b) => (\n                  <Chip\n                    key={b.id}\n                    pressed={form.budget === b.id}\n                    onClick={() => setForm((f) => ({ ...f, budget: b.id }))}\n                  >\n                    {b.label}\n                  </Chip>\n                ))}\n              </div>\n            </div>\n            <div>\n              <p className=\"mb-2 text-xs font-medium text-muted-foreground\">\n                Timeline\n              </p>\n              <div className=\"flex flex-wrap gap-2\">\n                {TIMELINES.map((t) => (\n                  <Chip\n                    key={t.id}\n                    pressed={form.timeline === t.id}\n                    onClick={() => setForm((f) => ({ ...f, timeline: t.id }))}\n                  >\n                    {t.label}\n                  </Chip>\n                ))}\n              </div>\n            </div>\n          </div>\n            ) : null}\n\n            {step === 3 ? (\n          <dl className=\"space-y-3 text-sm\">\n            <div className=\"flex justify-between gap-4 border-b border-border-subtle pb-2\">\n              <dt className=\"text-muted-foreground\">Name</dt>\n              <dd className=\"font-medium text-foreground\">{form.name}</dd>\n            </div>\n            <div className=\"flex justify-between gap-4 border-b border-border-subtle pb-2\">\n              <dt className=\"text-muted-foreground\">Email</dt>\n              <dd className=\"font-medium text-foreground\">{form.email}</dd>\n            </div>\n            <div className=\"flex justify-between gap-4 border-b border-border-subtle pb-2\">\n              <dt className=\"text-muted-foreground\">Project</dt>\n              <dd className=\"font-medium text-foreground\">\n                {PROJECT_TYPES.find((p) => p.id === form.projectType)?.label ??\n                  \"Not set\"}\n              </dd>\n            </div>\n            <div className=\"border-b border-border-subtle pb-2\">\n              <dt className=\"text-muted-foreground\">Note</dt>\n              <dd className=\"mt-1 text-foreground\">{form.message}</dd>\n            </div>\n          </dl>\n            ) : null}\n          </motion.div>\n        </AnimatePresence>\n\n        {status.kind === \"error\" ? (\n          <p className=\"text-sm text-red-600 dark:text-red-400\" role=\"alert\">\n            {status.message}\n          </p>\n        ) : null}\n\n        <div className=\"flex flex-wrap items-center justify-between gap-3 pt-2\">\n          <button\n            type=\"button\"\n            disabled={step === 0 || status.kind === \"submitting\"}\n            onClick={() => setStep((s) => Math.max(0, s - 1))}\n            className=\"inline-flex h-10 items-center gap-1.5 px-3 text-sm font-medium text-muted-foreground disabled:opacity-40\"\n          >\n            <ArrowLeft className=\"size-3.5\" aria-hidden />\n            Back\n          </button>\n          <button\n            type=\"submit\"\n            disabled={\n              status.kind === \"submitting\" || (step < 3 && !canNext())\n            }\n            className=\"inline-flex h-11 min-w-[7.5rem] items-center justify-center gap-2 rounded-lg bg-foreground px-5 text-sm font-medium text-background disabled:opacity-50\"\n          >\n            <AnimatePresence mode=\"wait\" initial={false}>\n              <motion.span\n                key={\n                  status.kind === \"submitting\"\n                    ? \"submitting\"\n                    : step === 3\n                      ? \"send\"\n                      : \"continue\"\n                }\n                className=\"inline-flex items-center gap-2\"\n                initial={reduce ? false : { opacity: 0, y: 4 }}\n                animate={{ opacity: 1, y: 0 }}\n                exit={reduce ? undefined : { opacity: 0, y: -4 }}\n                transition={{ duration: 0.16 }}\n              >\n                {status.kind === \"submitting\" ? (\n                  <>\n                    <Loader2 className=\"size-4 animate-spin\" aria-hidden />\n                    Sending…\n                  </>\n                ) : step === 3 ? (\n                  <>\n                    {CONTENT.submitLabel}\n                    <Send className=\"size-3.5\" aria-hidden />\n                  </>\n                ) : (\n                  <>\n                    Continue\n                    <ArrowRight className=\"size-3.5\" aria-hidden />\n                  </>\n                )}\n              </motion.span>\n            </AnimatePresence>\n          </button>\n        </div>\n      </form>\n    </section>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/blocks/contact-form.tsx"
    }
  ],
  "type": "registry:block"
}