{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "contact-form-teletype",
  "title": "Contact Form Teletype",
  "description": "A contact form whose validation prints as a teletype receipt below it: each field's check accretes as a new line, and correcting a field strikes through the old line and reprints rather than clearing it.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/contact-form-teletype/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useId, useRef, useState, type FormEvent } from \"react\";\n\n// ---------------------------------------------------------------------------\n// TeletypeForm — a contact form whose validation surfaces as a real running\n// receipt, not a red border. Every field validates on a debounced keystroke\n// (or on submit, for whatever hasn't fired yet) and PRINTS a new line below\n// the form — teletype-style, each line typing itself out over a duration\n// proportional to its own length via a width transition stepped one\n// character at a time, never cleared once written. Correcting a field does\n// NOT erase its old line: the old line gets struck through in place and a\n// fresh line for the same field prints below it, so the receipt reads as an\n// honest log of the fill (what was tried, in order) rather than a status\n// badge that overwrites itself.\n//\n// Distinct from validation-inline-wick (a single field's border diffuses a\n// tint from the offending character — no log, no history) and from\n// validation-error-summary (a punch list gathered at the TOP of the form on\n// failed submit, with leader lines back to fields, that self-dismisses once\n// every field resolves and forgets earlier attempts). Distinct from\n// ticker-teleprinter (a marquee crawl with no semantic content) and\n// progress-telegraph-log (a log of a caller-driven async operation's own\n// sub-steps, not of user-typed field validation). Here the log is the point:\n// it accretes for the life of the form, keeping every correction visible.\n// ---------------------------------------------------------------------------\n\nexport interface TeletypeFieldSpec {\n  name: string;\n  label: string;\n  type?: \"text\" | \"email\" | \"textarea\";\n  placeholder?: string;\n  /** return an error message, or null when the value passes */\n  validate: (value: string) => string | null;\n}\n\nexport interface ContactFormTeletypeProps {\n  /** the form fields, in order */\n  fields?: TeletypeFieldSpec[];\n  /** called with all field values, keyed by field id, on submit */\n  onSubmit?: (values: Record<string, string>) => void;\n  /** ms after the last keystroke before that field's line prints. Default 260. */\n  debounceMs?: number;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}\n\nconst DEFAULT_FIELDS: TeletypeFieldSpec[] = [\n  {\n    name: \"name\",\n    label: \"Name\",\n    validate: (v) => (v.trim().length === 0 ? \"required\" : v.trim().length < 2 ? \"too short\" : null),\n  },\n  {\n    name: \"email\",\n    label: \"Email\",\n    type: \"email\",\n    validate: (v) =>\n      v.trim().length === 0 ? \"required\" : /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(v) ? null : \"not a valid address\",\n  },\n  {\n    name: \"message\",\n    label: \"Message\",\n    type: \"textarea\",\n    validate: (v) => (v.trim().length === 0 ? \"required\" : v.trim().length < 10 ? \"too short\" : null),\n  },\n];\n\ninterface Entry {\n  id: number;\n  field: string;\n  label: string;\n  ok: boolean;\n  text: string;\n  struck: boolean;\n}\n\nfunction receiptLine(label: string, ok: boolean, err: string | null): string {\n  const dots = \".\".repeat(Math.max(3, 22 - label.length));\n  return ok ? `${label.toUpperCase()} ${dots} OK` : `${label.toUpperCase()} ${dots} FAIL — ${err}`;\n}\n\nexport function ContactFormTeletype({\n  fields = DEFAULT_FIELDS,\n  onSubmit,\n  debounceMs = 260,\n  className = \"\",\n}: ContactFormTeletypeProps) {\n  const uid = useId();\n  const [values, setValues] = useState<Record<string, string>>(() =>\n    Object.fromEntries(fields.map((f) => [f.name, \"\"]))\n  );\n  const [entries, setEntries] = useState<Entry[]>([]);\n  const debounceRefs = useRef<Record<string, ReturnType<typeof setTimeout> | undefined>>({});\n  const entryIdRef = useRef(0);\n  const reducedRef = useRef(false);\n\n  useEffect(() => {\n    reducedRef.current = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    const refs = debounceRefs.current;\n    return () => {\n      Object.values(refs).forEach((t) => t && clearTimeout(t));\n    };\n  }, []);\n\n  function printLine(field: TeletypeFieldSpec, value: string) {\n    const err = field.validate(value);\n    const ok = err === null;\n    setEntries((prev) => {\n      const struck = prev.map((e) => (e.field === field.name ? { ...e, struck: true } : e));\n      return [\n        ...struck,\n        {\n          id: entryIdRef.current++,\n          field: field.name,\n          label: field.label,\n          ok,\n          text: receiptLine(field.label, ok, err),\n          struck: false,\n        },\n      ];\n    });\n  }\n\n  function handleChange(field: TeletypeFieldSpec, next: string) {\n    setValues((v) => ({ ...v, [field.name]: next }));\n    const refs = debounceRefs.current;\n    if (refs[field.name]) clearTimeout(refs[field.name]);\n    refs[field.name] = setTimeout(() => printLine(field, next), debounceMs);\n  }\n\n  function handleSubmit(e: FormEvent) {\n    e.preventDefault();\n    let allOk = true;\n    fields.forEach((f) => {\n      const refs = debounceRefs.current;\n      if (refs[f.name]) clearTimeout(refs[f.name]);\n      const v = values[f.name] ?? \"\";\n      const err = f.validate(v);\n      if (err) allOk = false;\n      printLine(f, v);\n    });\n    if (allOk) onSubmit?.(values);\n  }\n\n  const fieldCls =\n    \"ns-cft-field w-full rounded-sm border border-border bg-background px-3 py-2 font-mono text-sm text-foreground transition-colors duration-150 placeholder:text-ns-muted/60 hover:border-ns-muted focus-visible:border-ns-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent\";\n\n  return (\n    <div className={`ns-cft w-full ${className}`}>\n      <style>{CSS}</style>\n      <form onSubmit={handleSubmit} className=\"flex flex-col gap-4\" noValidate>\n        {fields.map((field) => {\n          const fieldId = `${uid}-${field.name}`;\n          return (\n            <div key={field.name} className=\"flex flex-col gap-1.5\">\n              <label htmlFor={fieldId} className=\"font-mono text-xs uppercase tracking-widest text-ns-muted\">\n                {field.label}\n              </label>\n              {field.type === \"textarea\" ? (\n                <textarea\n                  id={fieldId}\n                  name={field.name}\n                  rows={3}\n                  value={values[field.name]}\n                  placeholder={field.placeholder}\n                  onChange={(e) => handleChange(field, e.target.value)}\n                  className={`${fieldCls} resize-none`}\n                />\n              ) : (\n                <input\n                  id={fieldId}\n                  name={field.name}\n                  type={field.type ?? \"text\"}\n                  value={values[field.name]}\n                  placeholder={field.placeholder}\n                  onChange={(e) => handleChange(field, e.target.value)}\n                  className={fieldCls}\n                />\n              )}\n            </div>\n          );\n        })}\n        <button\n          type=\"submit\"\n          className=\"ns-cft-submit self-start rounded-sm border border-border px-4 py-2 font-mono text-xs uppercase tracking-widest text-foreground transition-colors duration-150 hover:border-ns-accent hover:text-ns-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent\"\n        >\n          Send\n        </button>\n      </form>\n\n      <div\n        role=\"log\"\n        aria-live=\"polite\"\n        aria-label=\"Field validation receipt\"\n        className=\"ns-cft-receipt mt-6 flex flex-col gap-0.5 rounded-sm border border-border bg-surface p-3 font-mono text-xs\"\n      >\n        <p className=\"pb-1 text-ns-muted\">— RECEIPT —</p>\n        {entries.length === 0 && (\n          <p aria-hidden=\"true\" className=\"text-ns-muted/60\">\n            (awaiting input)\n          </p>\n        )}\n        {entries.map((entry) => (\n          <ReceiptLine key={entry.id} entry={entry} reduced={reducedRef.current} />\n        ))}\n      </div>\n    </div>\n  );\n}\n\nfunction ReceiptLine({ entry, reduced }: { entry: Entry; reduced: boolean }) {\n  const [printed, setPrinted] = useState(reduced);\n  const chars = Math.max(1, entry.text.length);\n\n  useEffect(() => {\n    if (reduced) return;\n    const raf = requestAnimationFrame(() => setPrinted(true));\n    return () => cancelAnimationFrame(raf);\n  }, [reduced]);\n\n  const duration = reduced ? 0 : Math.min(900, Math.max(160, chars * 20));\n\n  return (\n    <div\n      className=\"ns-cft-line\"\n      style={{ color: entry.ok ? \"var(--foreground)\" : \"var(--error, #ea001d)\" }}\n    >\n      <span\n        className={`ns-cft-print inline-block overflow-hidden whitespace-pre align-top ${\n          entry.struck ? \"ns-cft-struck\" : \"\"\n        }`}\n        style={{\n          width: printed ? `${chars}ch` : \"0ch\",\n          transition: reduced ? \"none\" : `width ${duration}ms steps(${chars}, end)`,\n        }}\n      >\n        {entry.text}\n      </span>\n    </div>\n  );\n}\n\nconst CSS = `\n.ns-cft-struck { text-decoration: line-through; text-decoration-thickness: 1px; opacity: 0.55; transition: opacity 200ms ease; }\n`;\n",
      "type": "registry:ui",
      "target": "components/ui/contact-form-teletype.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-muted": "var(--ns-muted)",
      "color-ns-accent": "var(--ns-accent)",
      "color-surface": "var(--surface)"
    },
    "light": {
      "ns-muted": "#4d4d4d",
      "ns-accent": "#006bff",
      "surface": "#fafafa",
      "error": "#ea001d"
    },
    "dark": {
      "ns-muted": "#8f8f8f",
      "surface": "#171717",
      "error": "#ff6369"
    }
  },
  "meta": {
    "collection": "core",
    "tags": [
      "form",
      "contact",
      "validation",
      "mono",
      "log",
      "accessibility",
      "teletype"
    ],
    "instruction": "<ContactFormTeletype fields? onSubmit? debounceMs? className?> renders a real <form> (Name, Email, Message by default, each with its own validate(value) returning an error string or null) and, below it, a role=log aria-live=polite receipt panel. 260ms after the last keystroke in a field (debounceMs), that field's validate runs and a new dot-leader line prints into the receipt — 'NAME .......... OK' or 'EMAIL .......... FAIL — not a valid address' — via a width transition stepped one character at a time (steps(N, end)) so it reads as typed rather than faded in. Submitting also validates and prints a line for every field regardless of debounce state. Nothing is ever erased: when a field is corrected and revalidated, its previous receipt line gets a strike-through and stays exactly where it was, and the new result prints as a fresh line below it — the receipt is a real cumulative log of the fill, not a status that overwrites itself. Colors: --foreground for a passing line, --error for a failing one; the form fields themselves use --border/--ns-muted/--ns-accent for resting/hover/focus, no hardcoded hex anywhere. Under prefers-reduced-motion every line renders at full width instantly with no step transition. Zero dependencies, no canvas."
  },
  "type": "registry:ui"
}