{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "agency-enquiry",
  "title": "Agency: Contact page",
  "description": "The email and the studio's address beside a project enquiry form with budget and timeline, checked on submit, that gives way to a sent state.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://eased.dev/r/eased-tokens.json",
    "https://eased.dev/r/eased-motion.json",
    "https://eased.dev/r/agency-core.json",
    "https://eased.dev/r/button.json",
    "https://eased.dev/r/field.json",
    "https://eased.dev/r/input.json",
    "https://eased.dev/r/select.json",
    "https://eased.dev/r/textarea.json"
  ],
  "files": [
    {
      "path": "src/registry/eased/templates/agency/enquiry.tsx",
      "content": "\"use client\";\n\nimport { AnimatePresence, motion } from \"motion/react\";\nimport { useRef, useState, type FormEvent } from \"react\";\nimport { Button } from \"../../button/button\";\nimport { Field } from \"../../field/field\";\nimport { Input } from \"../../input/input\";\nimport { enter, exitFast, reducedFade, useSafeReducedMotion } from \"../../lib/motion\";\nimport { Select } from \"../../select/select\";\nimport { Textarea } from \"../../textarea/textarea\";\nimport { contactPage } from \"./content-contact\";\nimport { ArrowLink, COLUMN, LABEL, PageHead, Reveal } from \"./kit\";\n\nconst { form, visit, email, jobs } = contactPage;\n\n/**\n * The contact page, /contact: the email and the studio's address on the left, a project enquiry\n * form on the right. The form checks itself on submit (rule 72): each error sits under its\n * field and the first one takes focus. Once sent, it gives way to a thank-you you can send\n * another from.\n */\nexport function ContactPage() {\n  return (\n    <section aria-labelledby=\"page-title\" className={`${COLUMN} pt-section-sm pb-group`}>\n      <PageHead title={contactPage.title} lead={contactPage.lead} />\n      <div className=\"mt-group grid gap-group border-t border-line-subtle pt-pad-lg lg:grid-cols-[minmax(0,5fr)_minmax(0,7fr)] lg:gap-group-lg\">\n        <Reveal className=\"flex flex-col gap-pad-lg\">\n          <div className=\"flex flex-col gap-1.5\">\n            <h2 className={LABEL}>{email.label}</h2>\n            <a\n              href={email.href}\n              className=\"focus-ring self-start rounded-control-xs font-display text-display-2xl font-medium tracking-display break-all text-text underline decoration-line-strong decoration-2 underline-offset-[0.18em] transition-colors duration-(--eased-duration-fast) ease-standard hover-fine:decoration-text\"\n            >\n              {email.value}\n            </a>\n          </div>\n          <div className=\"flex flex-col gap-1.5\">\n            <h2 className={LABEL}>{visit.label}</h2>\n            <address className=\"flex flex-col text-ui-md text-text not-italic\">\n              {visit.address.map((line) => (\n                <span key={line}>{line}</span>\n              ))}\n            </address>\n            <p className=\"max-w-sm text-ui text-pretty text-text-secondary\">\n              {visit.hours}. {visit.directions}\n            </p>\n          </div>\n          <div className=\"flex flex-col items-start gap-1.5\">\n            <h2 className={LABEL}>{jobs.label}</h2>\n            <p className=\"text-ui text-text-secondary\">{jobs.body}</p>\n            <ArrowLink href={jobs.link.href}>{jobs.link.label}</ArrowLink>\n          </div>\n        </Reveal>\n        <Reveal index={1}>\n          <Enquiry />\n        </Reveal>\n      </div>\n    </section>\n  );\n}\n\ntype Values = { name: string; email: string; company: string; service: string | null; budget: string | null; timeline: string | null; message: string };\ntype Key = keyof Values;\n\nconst EMPTY: Values = { name: \"\", email: \"\", company: \"\", service: null, budget: null, timeline: null, message: \"\" };\n/** In the form's order, so the first error found is the first one on the page. */\nconst ORDER: Key[] = [\"name\", \"email\", \"company\", \"service\", \"budget\", \"timeline\", \"message\"];\n\nconst options = (list: readonly string[]) => list.map((value) => ({ value, label: value }));\n\n/** Each field's error for these values, or none. */\nfunction validate(v: Values) {\n  const f = form.fields;\n  const errors: Partial<Record<Key, string>> = {};\n  if (!v.name.trim()) errors.name = f.name.required;\n  if (!v.email.trim()) errors.email = f.email.required;\n  else if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(v.email.trim())) errors.email = f.email.invalid;\n  if (!v.service) errors.service = f.service.required;\n  if (!v.budget) errors.budget = f.budget.required;\n  if (!v.timeline) errors.timeline = f.timeline.required;\n  if (!v.message.trim()) errors.message = f.message.required;\n  else if (v.message.trim().length < 20) errors.message = f.message.short;\n  return errors;\n}\n\n/** The enquiry form, and its sent state. Wire `submit` to your inbox or CRM. */\nfunction Enquiry() {\n  const reduced = useSafeReducedMotion();\n  const ref = useRef<HTMLFormElement>(null);\n  const [values, setValues] = useState(EMPTY);\n  const [errors, setErrors] = useState<Partial<Record<Key, string>>>({});\n  const [sent, setSent] = useState(false);\n  const f = form.fields;\n\n  const set = <K extends Key>(key: K, value: Values[K]) => {\n    setValues((v) => ({ ...v, [key]: value }));\n    setErrors((e) => ({ ...e, [key]: undefined }));\n  };\n\n  const submit = (e: FormEvent<HTMLFormElement>) => {\n    e.preventDefault();\n    const found = validate(values);\n    setErrors(found);\n    const first = ORDER.find((k) => found[k]);\n    if (first) {\n      // The field's own control: the input, the textarea or the select's trigger.\n      ref.current?.querySelector<HTMLElement>(`[data-field=\"${first}\"] :is(input:not([type=hidden]), textarea, button)`)?.focus();\n      return;\n    }\n    setSent(true);\n  };\n\n  const again = () => {\n    setValues(EMPTY);\n    setSent(false);\n  };\n\n  return (\n    <AnimatePresence mode=\"wait\" initial={false}>\n      {sent ? (\n        <motion.div\n          key=\"sent\"\n          role=\"status\"\n          className=\"flex flex-col items-start gap-pad-base rounded-panel bg-surface-sunken p-pad-lg\"\n          initial={{ opacity: 0, y: reduced ? 0 : 8 }}\n          animate={{ opacity: 1, y: 0 }}\n          transition={reduced ? reducedFade : enter}\n        >\n          <p className=\"font-display text-display-3xl font-medium tracking-display text-balance text-text\">{form.sent.title}</p>\n          <p className=\"max-w-lg text-ui-md text-pretty text-text-secondary\">{form.sent.body}</p>\n          <Button className=\"mt-in\" onClick={again}>\n            {form.sent.again}\n          </Button>\n        </motion.div>\n      ) : (\n        <motion.form\n          key=\"form\"\n          ref={ref}\n          noValidate\n          aria-label={form.label}\n          onSubmit={submit}\n          className=\"grid gap-x-pad-md gap-y-pad-md sm:grid-cols-2\"\n          exit={{ opacity: 0, transition: exitFast }}\n        >\n          <div data-field=\"name\">\n            <Field label={f.name.label} error={errors.name}>\n              <Input name=\"name\" autoComplete=\"name\" placeholder={f.name.placeholder} value={values.name} onChange={(e) => set(\"name\", e.target.value)} />\n            </Field>\n          </div>\n          <div data-field=\"email\">\n            <Field label={f.email.label} error={errors.email}>\n              <Input name=\"email\" type=\"email\" autoComplete=\"email\" placeholder={f.email.placeholder} value={values.email} onChange={(e) => set(\"email\", e.target.value)} />\n            </Field>\n          </div>\n          <div data-field=\"company\" className=\"sm:col-span-2\">\n            <Field label={f.company.label}>\n              <Input name=\"company\" autoComplete=\"organization\" placeholder={f.company.placeholder} value={values.company} onChange={(e) => set(\"company\", e.target.value)} />\n            </Field>\n          </div>\n          <div data-field=\"service\" className=\"sm:col-span-2\">\n            <Field label={f.service.label} error={errors.service}>\n              <Select items={options(form.services)} placeholder={f.service.placeholder} value={values.service} onValueChange={(v) => set(\"service\", v)} />\n            </Field>\n          </div>\n          <div data-field=\"budget\">\n            <Field label={f.budget.label} error={errors.budget}>\n              <Select items={options(form.budgets)} placeholder={f.budget.placeholder} value={values.budget} onValueChange={(v) => set(\"budget\", v)} />\n            </Field>\n          </div>\n          <div data-field=\"timeline\">\n            <Field label={f.timeline.label} error={errors.timeline}>\n              <Select items={options(form.timelines)} placeholder={f.timeline.placeholder} value={values.timeline} onValueChange={(v) => set(\"timeline\", v)} />\n            </Field>\n          </div>\n          <div data-field=\"message\" className=\"sm:col-span-2\">\n            <Field label={f.message.label} error={errors.message}>\n              <Textarea name=\"message\" minRows={5} placeholder={f.message.placeholder} value={values.message} onValueChange={(v) => set(\"message\", v)} />\n            </Field>\n          </div>\n          <div className=\"flex flex-wrap items-center gap-x-pad-md gap-y-in sm:col-span-2\">\n            <Button type=\"submit\" variant=\"primary\" size=\"lg\">\n              {form.submit}\n            </Button>\n            <p className=\"text-ui-sm text-text-muted\">{form.note}</p>\n          </div>\n        </motion.form>\n      )}\n    </AnimatePresence>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/eased/templates/agency/enquiry.tsx"
    }
  ],
  "meta": {
    "template": "agency",
    "kind": "template-section",
    "industries": [
      "media"
    ]
  },
  "type": "registry:block"
}