{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "contact-1",
  "title": "Contact 1: form with contact methods",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A contact section with a validated form (name, email, topic, message with counter, consent), loading, error and thank-you states, beside tiles for email, chat, phone and office with a response-time pill.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/button.json",
    "https://ui.ballmac.com/r/checkbox.json",
    "https://ui.ballmac.com/r/field.json",
    "https://ui.ballmac.com/r/input.json",
    "https://ui.ballmac.com/r/select.json",
    "https://ui.ballmac.com/r/status-dot.json",
    "https://ui.ballmac.com/r/textarea.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/contact-1/contact-1.tsx",
      "content": "// Ballmac UI: Contact 1. https://ui.ballmac.com/blocks/contact-1\n\"use client\"\n\nimport * as React from \"react\"\nimport { ArrowRight, Check, Mail, MapPin, MessageCircle, Phone } from \"lucide-react\"\n\nimport { Button } from \"@/components/ballmac/button\"\nimport { Checkbox } from \"@/components/ballmac/checkbox\"\nimport { Field, FieldDescription, FieldError, FieldGroup, FieldLabel, useFieldControl } from \"@/components/ballmac/field\"\nimport { Input } from \"@/components/ballmac/input\"\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ballmac/select\"\nimport { StatusDot } from \"@/components/ballmac/status-dot\"\nimport { Textarea } from \"@/components/ballmac/textarea\"\nimport { cn } from \"@/lib/utils\"\n\ntype Contact1Values = { name: string; email: string; topic: string; message: string }\n\ntype Contact1Method = {\n  /** Icon in the tile. */\n  icon?: React.ReactNode\n  label: string\n  /** The detail, e.g. an address. */\n  value: string\n  /** Where it links to (mailto:, tel:, a chat page). */\n  href?: string\n}\n\ntype Contact1Props = Omit<React.ComponentProps<\"section\">, \"title\" | \"onSubmit\"> & {\n  /** Section heading. */\n  title?: string\n  /** One or two sentences under the heading. */\n  description?: string\n  /** Ways to reach you besides the form. */\n  methods?: Contact1Method[]\n  /** Topics in the dropdown. */\n  topics?: string[]\n  /** How quickly you reply, shown with a status dot. Pass null to hide it. */\n  responseTime?: string | null\n  /** Longest message in characters. */\n  maxLength?: number\n  /** Called with the validated values. Throw to show an error; resolve to show the thank-you screen. Defaults to a short simulated delay. */\n  onSubmit?: (values: Contact1Values) => void | Promise<void>\n}\n\nconst defaultMethods: Contact1Method[] = [\n  { icon: <Mail />, label: \"Email\", value: \"hello@acme.com\", href: \"mailto:hello@acme.com\" },\n  { icon: <MessageCircle />, label: \"Live chat\", value: \"Weekdays, 9am to 6pm CET\", href: \"#\" },\n  { icon: <Phone />, label: \"Phone\", value: \"+351 21 000 0000\", href: \"tel:+351210000000\" },\n  { icon: <MapPin />, label: \"Office\", value: \"Rua das Flores 12, Lisbon\", href: \"#\" },\n]\n\nfunction FieldInput(props: React.ComponentProps<typeof Input>) {\n  return <Input {...useFieldControl()} {...props} />\n}\nfunction TopicTrigger(props: React.ComponentProps<typeof SelectTrigger>) {\n  const { id, disabled, ...aria } = useFieldControl()\n  return <SelectTrigger id={id} disabled={disabled} className=\"w-full\" {...aria} {...props} />\n}\nfunction MessageBox(props: React.ComponentProps<typeof Textarea>) {\n  return <Textarea {...useFieldControl()} {...props} />\n}\n\nconst emailPattern = /^\\S+@\\S+\\.\\S+$/\n\nfunction validate(v: Contact1Values, consent: boolean, topics: string[]) {\n  const errors: Partial<Record<keyof Contact1Values | \"consent\", string>> = {}\n  if (!v.name.trim()) errors.name = \"Tell us your name.\"\n  if (!emailPattern.test(v.email)) errors.email = \"Enter a full email address, like name@acme.com.\"\n  if (topics.length && !v.topic) errors.topic = \"Choose what this is about.\"\n  if (v.message.trim().length < 10) errors.message = \"Write at least a sentence so we can help.\"\n  if (!consent) errors.consent = \"Please agree so we can reply to you.\"\n  return errors\n}\n\nfunction Contact1({\n  title = \"Let’s talk.\",\n  description = \"Questions about pricing, a partnership or something that isn’t working? Send a note and a real person will reply.\",\n  methods = defaultMethods,\n  topics = [\"Sales\", \"Support\", \"Partnerships\", \"Press\", \"Something else\"],\n  responseTime = \"We usually reply within 4 hours\",\n  maxLength = 600,\n  onSubmit,\n  className,\n  ...props\n}: Contact1Props) {\n  const [values, setValues] = React.useState<Contact1Values>({ name: \"\", email: \"\", topic: \"\", message: \"\" })\n  const [consent, setConsent] = React.useState(false)\n  const [touched, setTouched] = React.useState<Record<string, boolean>>({})\n  const [state, setState] = React.useState<\"idle\" | \"sending\" | \"done\" | \"error\">(\"idle\")\n  const formRef = React.useRef<HTMLFormElement>(null)\n  const errors = validate(values, consent, topics)\n  const show = (k: keyof typeof errors) => (touched[k] ? errors[k] : undefined)\n  const set = <K extends keyof Contact1Values>(k: K, v: Contact1Values[K]) => setValues((p) => ({ ...p, [k]: v }))\n\n  async function submit(e: React.FormEvent) {\n    e.preventDefault()\n    setTouched({ name: true, email: true, topic: true, message: true, consent: true })\n    const first = Object.keys(errors)[0]\n    if (first) {\n      formRef.current?.querySelector<HTMLElement>(`[data-field=\"${first}\"] input, [data-field=\"${first}\"] textarea, [data-field=\"${first}\"] button`)?.focus()\n      return\n    }\n    setState(\"sending\")\n    try {\n      if (onSubmit) await onSubmit(values)\n      else await new Promise((r) => setTimeout(r, 900))\n      setState(\"done\")\n    } catch {\n      setState(\"error\")\n    }\n  }\n\n  function reset() {\n    setValues({ name: \"\", email: \"\", topic: \"\", message: \"\" })\n    setConsent(false)\n    setTouched({})\n    setState(\"idle\")\n  }\n\n  return (\n    <section data-slot=\"contact-1\" className={cn(\"mx-auto max-w-6xl px-4 py-20 sm:px-6 md:py-28\", className)} {...props}>\n      <div className=\"grid gap-12 lg:grid-cols-[1fr_1.15fr] lg:gap-16\">\n        <div className=\"min-w-0\">\n          <h2 className=\"text-3xl font-semibold tracking-[-0.035em] text-balance sm:text-4xl lg:text-5xl\">{title}</h2>\n          <p className=\"text-muted-foreground mt-4 max-w-md text-lg text-pretty\">{description}</p>\n          {responseTime && (\n            <div className=\"mt-6 inline-flex rounded-full border px-3.5 py-1.5\">\n              <StatusDot status=\"online\" pulse label={responseTime} className=\"text-sm\" />\n            </div>\n          )}\n          <ul className=\"mt-10 grid gap-3 sm:grid-cols-2 lg:grid-cols-1 xl:grid-cols-2\">\n            {methods.map((m) => (\n              <li key={m.label}>\n                <a\n                  href={m.href}\n                  className=\"group/method hover:bg-accent focus-visible:ring-ring/50 flex h-full items-start gap-4 rounded-2xl border p-4 outline-none transition-colors focus-visible:ring-[3px]\"\n                >\n                  <span aria-hidden=\"true\" className=\"bg-muted flex size-10 shrink-0 items-center justify-center rounded-xl [&_svg]:size-5\">{m.icon ?? <Mail />}</span>\n                  <span className=\"min-w-0\">\n                    <span className=\"block text-sm font-medium\">{m.label}</span>\n                    <span className=\"text-muted-foreground block text-sm break-words\">{m.value}</span>\n                  </span>\n                </a>\n              </li>\n            ))}\n          </ul>\n        </div>\n\n        <div className=\"bg-card relative min-w-0 rounded-3xl border p-6 shadow-[0_30px_80px_-50px_rgb(0_0_0/0.4)] sm:p-8\">\n          {state === \"done\" ? (\n            <div role=\"status\" className=\"flex min-h-[26rem] flex-col items-center justify-center text-center\">\n              <span className=\"bg-chart-2/15 flex size-14 items-center justify-center rounded-full\">\n                <Check className=\"size-7\" aria-hidden=\"true\" />\n              </span>\n              <h3 className=\"mt-6 text-2xl font-semibold tracking-[-0.02em]\">Message sent</h3>\n              <p className=\"text-muted-foreground mt-2 max-w-xs text-pretty\">Thanks, {values.name.split(\" \")[0]}. We’ll reply to {values.email} soon.</p>\n              <Button variant=\"outline\" shape=\"pill\" className=\"mt-8\" onClick={reset}>Send another message</Button>\n            </div>\n          ) : (\n            <form ref={formRef} noValidate onSubmit={submit} aria-label=\"Contact form\">\n              <FieldGroup>\n                <div className=\"grid gap-5 sm:grid-cols-2\">\n                  <Field invalid={!!show(\"name\")} data-field=\"name\">\n                    <FieldLabel required>Name</FieldLabel>\n                    <FieldInput autoComplete=\"name\" placeholder=\"Jordan Lee\" value={values.name} onChange={(e) => set(\"name\", e.target.value)} onBlur={() => setTouched((t) => ({ ...t, name: true }))} />\n                    <FieldError errors={[show(\"name\")]} />\n                  </Field>\n                  <Field invalid={!!show(\"email\")} data-field=\"email\">\n                    <FieldLabel required>Email</FieldLabel>\n                    <FieldInput type=\"email\" autoComplete=\"email\" placeholder=\"jordan@company.com\" value={values.email} onChange={(e) => set(\"email\", e.target.value)} onBlur={() => setTouched((t) => ({ ...t, email: true }))} />\n                    <FieldError errors={[show(\"email\")]} />\n                  </Field>\n                </div>\n                {topics.length > 0 && (\n                  <Field invalid={!!show(\"topic\")} data-field=\"topic\">\n                    <FieldLabel required>What is this about?</FieldLabel>\n                    <Select value={values.topic} onValueChange={(v) => { set(\"topic\", v); setTouched((t) => ({ ...t, topic: true })) }}>\n                      <TopicTrigger>\n                        <SelectValue placeholder=\"Choose a topic\" />\n                      </TopicTrigger>\n                      <SelectContent>\n                        {topics.map((t) => (\n                          <SelectItem key={t} value={t}>{t}</SelectItem>\n                        ))}\n                      </SelectContent>\n                    </Select>\n                    <FieldError errors={[show(\"topic\")]} />\n                  </Field>\n                )}\n                <Field invalid={!!show(\"message\")} data-field=\"message\">\n                  <FieldLabel required>Message</FieldLabel>\n                  <MessageBox rows={5} maxLength={maxLength} placeholder=\"How can we help?\" value={values.message} onChange={(e) => set(\"message\", e.target.value)} onBlur={() => setTouched((t) => ({ ...t, message: true }))} />\n                  <div className=\"flex items-start justify-between gap-3\">\n                    <FieldError errors={[show(\"message\")]} />\n                    <FieldDescription className=\"ms-auto text-xs tabular-nums\">{values.message.length}/{maxLength}</FieldDescription>\n                  </div>\n                </Field>\n                <Field orientation=\"horizontal\" invalid={!!show(\"consent\")} data-field=\"consent\">\n                  <Checkbox id=\"contact-consent\" checked={consent} onCheckedChange={(c) => { setConsent(c === true); setTouched((t) => ({ ...t, consent: true })) }} aria-invalid={!!show(\"consent\") || undefined} />\n                  <div className=\"grid gap-1\">\n                    <FieldLabel htmlFor=\"contact-consent\" className=\"font-normal\">I agree to be contacted about this request.</FieldLabel>\n                    <FieldError errors={[show(\"consent\")]} />\n                  </div>\n                </Field>\n                {state === \"error\" && <p role=\"alert\" className=\"text-destructive text-sm\">Something went wrong sending your message. Please try again, or email us directly.</p>}\n                <Button type=\"submit\" size=\"lg\" shape=\"pill\" loading={state === \"sending\"} className=\"w-full\">\n                  {state === \"sending\" ? \"Sending…\" : <>Send message <ArrowRight  className=\"rtl:rotate-180\"/></>}\n                </Button>\n              </FieldGroup>\n            </form>\n          )}\n        </div>\n      </div>\n    </section>\n  )\n}\n\nexport { Contact1, type Contact1Props, type Contact1Values, type Contact1Method }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/contact-1/contact-1.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "contact",
      "form",
      "support",
      "validation",
      "email",
      "sales"
    ],
    "version": "1.0.0",
    "updated": "2026-10-01",
    "ai": {
      "summary": "Contact page section. Pass methods, topics and onSubmit(values) (throw to show an error). Validation, focus on the first error, loading and the thank-you screen are built in.",
      "whenToUse": [
        "Contact and support pages",
        "Sales enquiry sections"
      ],
      "whenNotToUse": [
        "Newsletter signup (use newsletter-1)",
        "Authenticated in-app feedback (use feedback-widget)"
      ],
      "composesWith": [
        "faq-2",
        "header-2",
        "footer-2",
        "careers-1"
      ],
      "a11y": [
        {
          "keys": "Submit with errors",
          "action": "Focus moves to the first invalid field; each error is announced as an alert and linked with aria-describedby"
        },
        {
          "keys": "Arrow keys / Enter in the topic menu",
          "action": "Choose a topic (Radix Select)"
        }
      ],
      "customization": [
        "methods: { icon?, label, value, href? }[]",
        "topics: [] hides the dropdown",
        "responseTime: null hides the pill",
        "onSubmit(values): async; throw to show the error state"
      ]
    },
    "examples": [
      "contact-1-demo",
      "contact-1-support"
    ],
    "url": "https://ui.ballmac.com/components/contact-1"
  },
  "categories": [
    "blocks",
    "contact"
  ],
  "type": "registry:block"
}