{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cta-contact-morph",
  "type": "registry:block",
  "title": "CTA Contact Morph",
  "description": "A contact call-to-action whose button springs open into a fill-in-the-sentence message, then folds back into a sent confirmation. The block keeps the open size, so nothing on the page moves.",
  "dependencies": [
    "gsap",
    "@gsap/react"
  ],
  "registryDependencies": [],
  "cssVars": {
    "theme": {}
  },
  "files": [
    {
      "path": "components/tweenui/cta-contact-morph.tsx",
      "type": "registry:component",
      "target": "components/tweenui/cta-contact-morph.tsx",
      "content": "'use client';\n\nimport {\n  useEffect,\n  useId,\n  useRef,\n  useState,\n  type ComponentPropsWithoutRef,\n  type SubmitEvent,\n} from 'react';\nimport { useGSAP } from '@gsap/react';\nimport gsap from 'gsap';\nimport { cn } from '@/lib/utils';\n\ngsap.registerPlugin(useGSAP);\n\nconst OPEN = 0.56;\nconst CLOSE = 0.34;\nconst RADIUS = 28;\nconst CLIP_SHUT = `inset(50% 50% 50% 50% round ${RADIUS}px)`;\nconst CLIP_OPEN = `inset(0% 0% 0% 0% round ${RADIUS}px)`;\nconst EMAIL = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\nconst SPRING_EASE = 'ease-[cubic-bezier(0.34,1.56,0.64,1)]';\n\n// Bright underline grows from the left on focus over a dim one.\nconst FIELD_CLASS =\n  'ml-2 mr-0.5 inline-block min-w-[5ch] max-w-full bg-transparent bg-[linear-gradient(#c6f56f,#c6f56f),linear-gradient(#c6f56f73,#c6f56f73)] bg-[length:0%_2px,100%_2px] bg-[position:100%_100%,0_100%] bg-no-repeat px-0.5 pb-0.5 align-baseline [field-sizing:content] text-white outline-none transition-[background-size] duration-500 ease-[cubic-bezier(0.16,1,0.3,1)] placeholder:text-[#9fd4d6]/55 focus:bg-[length:100%_2px,100%_2px] focus:bg-[position:0_100%,0_100%] aria-invalid:bg-[linear-gradient(#ffb4a8,#ffb4a8),linear-gradient(#ffb4a8,#ffb4a8)] motion-reduce:transition-none';\n\n// Leading edge pulls hard, trailing edge lags: the pill stretches as it travels.\nconst LEAD = 520;\nconst TRAIL = 170;\n\ntype Spring = { x: number; v: number; t: number };\ntype Pill = { left: Spring; right: Spring; top: Spring; bottom: Spring };\n\nconst spring = (): Spring => ({ x: 0, v: 0, t: 0 });\nconst pill = (): Pill => ({ left: spring(), right: spring(), top: spring(), bottom: spring() });\nconst edges = (p: Pill) => [p.left, p.right, p.top, p.bottom];\n\nconst stepSpring = (s: Spring, stiffness: number, dt: number) => {\n  s.v += (stiffness * (s.t - s.x) - 2 * Math.sqrt(stiffness) * s.v) * dt;\n  s.x += s.v * dt;\n};\n\nconst pulls = (start: Spring, end: Spring) => {\n  const heading = start.t + end.t - start.x - end.x;\n  return [heading > 1 ? TRAIL : LEAD, heading < -1 ? TRAIL : LEAD] as const;\n};\n\nconst stepPill = (p: Pill, dt: number) => {\n  const [left, right] = pulls(p.left, p.right);\n  const [top, bottom] = pulls(p.top, p.bottom);\n  stepSpring(p.left, left, dt);\n  stepSpring(p.right, right, dt);\n  stepSpring(p.top, top, dt);\n  stepSpring(p.bottom, bottom, dt);\n};\n\nconst aim = (p: Pill, chip: HTMLElement, instant: boolean) => {\n  p.left.t = chip.offsetLeft;\n  p.right.t = chip.offsetLeft + chip.offsetWidth;\n  p.top.t = chip.offsetTop;\n  p.bottom.t = chip.offsetTop + chip.offsetHeight;\n  if (!instant) return;\n  for (const s of edges(p)) {\n    s.x = s.t;\n    s.v = 0;\n  }\n};\n\nconst atRest = (p: Pill) =>\n  edges(p).every((s) => Math.abs(s.t - s.x) < 0.05 && Math.abs(s.v) < 0.05);\n\nconst drawPill = (el: HTMLElement | null, p: Pill) => {\n  if (!el) return;\n  el.style.transform = `translate3d(${p.left.x}px,${p.top.x}px,0)`;\n  el.style.width = `${Math.max(0, p.right.x - p.left.x)}px`;\n  el.style.height = `${Math.max(0, p.bottom.x - p.top.x)}px`;\n};\n\nconst prefersReducedMotion = () =>\n  typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\n// Scale that shrinks the panel down to the button it grew from.\nconst tucked = (trigger: HTMLElement, panel: HTMLElement) => ({\n  scaleX: trigger.offsetWidth / panel.offsetWidth,\n  scaleY: trigger.offsetHeight / panel.offsetHeight,\n});\n\n// Counter-scales the radius so corners stay RADIUS while the panel scales.\nconst keepCorners = (panel: HTMLElement, surface: HTMLElement) => () => {\n  const sx = Number(gsap.getProperty(panel, 'scaleX')) || 1;\n  const sy = Number(gsap.getProperty(panel, 'scaleY')) || 1;\n  surface.style.borderRadius = `${RADIUS / sx}px / ${RADIUS / sy}px`;\n};\n\nconst wait = (ms: number) => new Promise<void>((resolve) => window.setTimeout(resolve, ms));\n\n// `size` is the width fallback where `field-sizing: content` isn't supported.\nconst fieldSize = (value: string, placeholder: string) => (value || placeholder).length;\n\nexport interface CtaContactMessage {\n  name: string;\n  email: string;\n  topic: string;\n  details: string;\n}\n\nexport interface CtaContactMorphProps extends ComponentPropsWithoutRef<'section'> {\n  buttonLabel?: string;\n  topics?: string[];\n  replyTime?: string;\n  onSend?: (message: CtaContactMessage) => void | Promise<void>;\n}\n\ntype Status = 'idle' | 'sending' | 'sent';\ntype FieldError = { field?: 'name' | 'email'; text: string };\n\ninterface TopicPickerProps {\n  name: string;\n  topics: string[];\n  value: string;\n  onChange: (topic: string) => void;\n}\n\nconst TopicPicker = ({ name, topics, value, onChange }: TopicPickerProps) => {\n  const groupRef = useRef<HTMLSpanElement>(null);\n  const selectedRef = useRef<HTMLSpanElement>(null);\n  const hoverRef = useRef<HTMLSpanElement>(null);\n  const chips = useRef<(HTMLSpanElement | null)[]>([]);\n  const motion = useRef({ selected: pill(), hover: pill(), placed: false, hovering: -1 });\n  const ticking = useRef(false);\n  const selectedIndex = Math.max(0, topics.indexOf(value));\n  const selectedNow = useRef(selectedIndex);\n  selectedNow.current = selectedIndex;\n\n  const draw = () => {\n    drawPill(selectedRef.current, motion.current.selected);\n    drawPill(hoverRef.current, motion.current.hover);\n  };\n\n  // The ticker only runs while a pill is moving.\n  const tick = useRef((_time: number, deltaTime: number) => {\n    const m = motion.current;\n    const elapsed = Math.min(deltaTime / 1000, 0.05);\n    const steps = Math.max(1, Math.ceil(elapsed * 120));\n    for (let step = 0; step < steps; step += 1) {\n      stepPill(m.selected, elapsed / steps);\n      stepPill(m.hover, elapsed / steps);\n    }\n    draw();\n    if (atRest(m.selected) && atRest(m.hover)) {\n      gsap.ticker.remove(tick.current);\n      ticking.current = false;\n    }\n  });\n\n  const moveTo = (p: Pill, index: number, instant: boolean) => {\n    const chip = chips.current[index];\n    if (!chip) return;\n    aim(p, chip, instant || prefersReducedMotion());\n    draw();\n    if (!ticking.current) {\n      ticking.current = true;\n      gsap.ticker.add(tick.current);\n    }\n  };\n\n  useEffect(() => {\n    const stop = tick.current;\n    // Rewrapped chips jump the pills to their new spots.\n    const group = groupRef.current;\n    const resize =\n      typeof ResizeObserver === 'undefined' || !group\n        ? null\n        : new ResizeObserver(() => {\n            const m = motion.current;\n            const chip = chips.current[selectedNow.current];\n            if (chip) aim(m.selected, chip, true);\n            const hovered = chips.current[m.hovering];\n            if (hovered) aim(m.hover, hovered, true);\n            draw();\n          });\n    if (group) resize?.observe(group);\n    return () => {\n      resize?.disconnect();\n      gsap.ticker.remove(stop);\n      ticking.current = false;\n    };\n  }, []);\n\n  useEffect(() => {\n    const m = motion.current;\n    moveTo(m.selected, selectedIndex, !m.placed);\n    m.placed = true;\n  }, [selectedIndex]);\n\n  const hoverOn = (index: number) => {\n    const m = motion.current;\n    // From outside the row it appears in place; across the row it slides.\n    moveTo(m.hover, index, m.hovering < 0);\n    m.hovering = index;\n    if (hoverRef.current) hoverRef.current.style.opacity = '1';\n  };\n\n  const hoverOff = () => {\n    motion.current.hovering = -1;\n    if (hoverRef.current) hoverRef.current.style.opacity = '0';\n  };\n\n  return (\n    <span\n      ref={groupRef}\n      role=\"radiogroup\"\n      aria-label=\"What you need help with\"\n      onPointerLeave={hoverOff}\n      className=\"relative -mt-0.5 flex flex-wrap gap-1.5\"\n    >\n      <span\n        ref={hoverRef}\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute top-0 left-0 rounded-full bg-white/10 opacity-0 transition-opacity duration-200 motion-reduce:transition-none\"\n      />\n      <span\n        ref={selectedRef}\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute top-0 left-0 rounded-full bg-[#c6f56f]\"\n      />\n      {topics.map((topic, index) => (\n        <label\n          key={topic}\n          className=\"cursor-pointer\"\n          onPointerEnter={(event) => {\n            if (event.pointerType === 'mouse') hoverOn(index);\n          }}\n        >\n          <input\n            type=\"radio\"\n            name={name}\n            value={topic}\n            checked={value === topic}\n            onChange={() => onChange(topic)}\n            className=\"peer sr-only\"\n          />\n          <span\n            ref={(el) => {\n              chips.current[index] = el;\n            }}\n            className=\"relative inline-block rounded-full border border-[#9fd4d6]/30 px-3 py-0.5 text-base leading-7 text-[#e8f3f2] transition-[color,border-color] duration-200 peer-checked:border-transparent peer-checked:text-[#0b3b3e] peer-focus-visible:ring-2 peer-focus-visible:ring-[#c6f56f] peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-[#0b3b3e] motion-reduce:transition-none\"\n          >\n            {topic}\n          </span>\n        </label>\n      ))}\n    </span>\n  );\n};\n\nconst CtaContactMorph = ({\n  buttonLabel = 'Write us a message',\n  topics = ['a website', 'a brand', 'an app', 'something else'],\n  replyTime = 'We reply within one working day.',\n  onSend = () => wait(900),\n  className,\n  ...props\n}: CtaContactMorphProps) => {\n  const ids = useId();\n  const rootRef = useRef<HTMLElement>(null);\n  const triggerRef = useRef<HTMLButtonElement>(null);\n  const panelRef = useRef<HTMLDivElement>(null);\n  const surfaceRef = useRef<HTMLDivElement>(null);\n  const shadowRef = useRef<HTMLDivElement>(null);\n  const formRef = useRef<HTMLFormElement>(null);\n  const nameRef = useRef<HTMLInputElement>(null);\n  const emailRef = useRef<HTMLInputElement>(null);\n  const timeline = useRef<gsap.core.Timeline | null>(null);\n  const live = useRef(false);\n  const [open, setOpen] = useState(false);\n  const [status, setStatus] = useState<Status>('idle');\n  const [error, setError] = useState<FieldError | null>(null);\n  const [message, setMessage] = useState<CtaContactMessage>({\n    name: '',\n    email: '',\n    topic: '',\n    details: '',\n  });\n\n  // Falls back to the first topic, and stays valid if `topics` changes.\n  const topic = topics.includes(message.topic) ? message.topic : (topics[0] ?? '');\n  const sent = status === 'sent';\n\n  const close = (refocus = false) => {\n    setOpen(false);\n    if (refocus) triggerRef.current?.focus();\n  };\n\n  useGSAP(\n    () => {\n      const trigger = triggerRef.current;\n      const panel = panelRef.current;\n      const surface = surfaceRef.current;\n      const form = formRef.current;\n      const shadow = shadowRef.current;\n      if (!trigger || !panel || !surface || !form || !shadow) return;\n      timeline.current?.kill();\n\n      if (prefersReducedMotion()) {\n        live.current = open;\n        gsap.set(trigger, { opacity: open ? 0 : 1 });\n        gsap.set(panel, { autoAlpha: open ? 1 : 0, scaleX: 1, scaleY: 1 });\n        gsap.set(surface, { scaleX: 1, scaleY: 1, borderRadius: RADIUS });\n        gsap.set(form, { clipPath: 'none', opacity: 1 });\n        gsap.set(shadow, { opacity: open ? 1 : 0 });\n        return;\n      }\n\n      if (open) {\n        // Reopening mid-close carries on from where the panel is.\n        if (!live.current) {\n          gsap.set(panel, tucked(trigger, panel));\n          gsap.set(surface, { scaleX: 0.86, scaleY: 1.14 });\n          gsap.set(form, { clipPath: CLIP_SHUT, opacity: 0 });\n        }\n        live.current = true;\n        gsap.set(panel, { autoAlpha: 1 });\n        const corners = keepCorners(panel, surface);\n        corners();\n\n        timeline.current = gsap\n          .timeline()\n          .to(trigger, { opacity: 0, duration: 0.15 }, 0)\n          .to(\n            panel,\n            { scaleX: 1, scaleY: 1, duration: OPEN, ease: 'back.out(1.3)', onUpdate: corners },\n            0\n          )\n          .to(\n            surface,\n            {\n              keyframes: {\n                '45%': { scaleX: 1.05, scaleY: 0.95 },\n                '70%': { scaleX: 0.985, scaleY: 1.015 },\n                '100%': { scaleX: 1, scaleY: 1 },\n                easeEach: 'back.out(1.7)',\n              },\n              duration: OPEN,\n              ease: 'none',\n            },\n            0\n          )\n          .to(form, { clipPath: CLIP_OPEN, opacity: 1, duration: 0.4, ease: 'power1.out' }, 0.06)\n          // The shadow waits for the panel to settle: painting a large blurred\n          // shadow while the panel scales stalls the first open by ~100ms.\n          .to(shadow, { opacity: 1, duration: 0.3, ease: 'power1.out' }, OPEN * 0.85);\n        return;\n      }\n\n      if (!live.current) {\n        gsap.set(panel, { autoAlpha: 0 });\n        return;\n      }\n\n      const corners = keepCorners(panel, surface);\n      timeline.current = gsap\n        .timeline({\n          onComplete: () => {\n            live.current = false;\n            gsap.set(panel, { autoAlpha: 0 });\n          },\n        })\n        .to(form, { clipPath: CLIP_SHUT, opacity: 0, duration: 0.2, ease: 'power1.in' }, 0)\n        .to(shadow, { opacity: 0, duration: 0.1 }, 0)\n        .to(\n          panel,\n          { ...tucked(trigger, panel), duration: CLOSE, ease: 'back.in(1.7)', onUpdate: corners },\n          0\n        )\n        .to(trigger, { opacity: 1, duration: 0.14 }, CLOSE * 0.7);\n    },\n    { scope: rootRef, dependencies: [open] }\n  );\n\n  useEffect(() => {\n    if (!open) return;\n    nameRef.current?.focus({ preventScroll: true });\n\n    // Only Escape pressed inside the panel closes it.\n    const onKeyDown = (event: KeyboardEvent) => {\n      if (event.key === 'Escape' && panelRef.current?.contains(document.activeElement)) {\n        close(true);\n      }\n    };\n    document.addEventListener('keydown', onKeyDown);\n    return () => {\n      document.removeEventListener('keydown', onKeyDown);\n    };\n  }, [open]);\n\n  const update = (field: keyof CtaContactMessage) => (value: string) => {\n    setMessage((current) => ({ ...current, [field]: value }));\n    if (error?.field === field) setError(null);\n  };\n\n  const submit = async (event: SubmitEvent<HTMLFormElement>) => {\n    event.preventDefault();\n    if (status === 'sending') return;\n    const name = message.name.trim();\n    const email = message.email.trim();\n    if (!name) {\n      setError({ field: 'name', text: 'Add your name so we know who to reply to.' });\n      nameRef.current?.focus();\n      return;\n    }\n    if (!EMAIL.test(email)) {\n      setError({ field: 'email', text: 'Add an email address we can reply to.' });\n      emailRef.current?.focus();\n      return;\n    }\n    setError(null);\n    setStatus('sending');\n    try {\n      await onSend({ ...message, name, email, topic });\n      setStatus('sent');\n      close(true);\n    } catch {\n      setStatus('idle');\n      setError({ text: 'Your message didn’t send. Check your connection and try again.' });\n      // If the panel was closed while sending, bring it back to show the error.\n      setOpen(true);\n    }\n  };\n\n  // Points a field at the error line only when the error is about it.\n  const describedBy = (field: 'name' | 'email') =>\n    error && (!error.field || error.field === field) ? `${ids}-error` : undefined;\n\n  return (\n    <section\n      {...props}\n      ref={rootRef}\n      data-cta-contact-morph\n      className={cn('w-full px-4 py-16 md:py-24', className)}\n    >\n      {/* Button and panel share one grid cell, so the block always keeps the\n          open panel's size and nothing around it moves. */}\n      <div className=\"mx-auto grid w-full max-w-[30rem] place-items-center\">\n        <button\n          ref={triggerRef}\n          type=\"button\"\n          aria-haspopup={sent ? undefined : 'dialog'}\n          aria-expanded={sent ? undefined : open}\n          aria-controls={sent ? undefined : `${ids}-panel`}\n          aria-disabled={sent || undefined}\n          onClick={() => {\n            if (!sent) setOpen(true);\n          }}\n          className={cn(\n            'group/trigger inline-flex h-14 items-center gap-4 rounded-full pr-2 pl-6 text-base font-medium transition-colors duration-200 outline-none [grid-area:1/1] focus-visible:ring-2 focus-visible:ring-[#045f64] focus-visible:ring-offset-2 motion-reduce:transition-none',\n            sent\n              ? 'cursor-default bg-[#c6f56f] text-[#0b3b3e]'\n              : 'cursor-pointer bg-[#045f64] text-[#c6f56f] hover:bg-[#034e52]'\n          )}\n        >\n          {sent ? 'Message sent' : buttonLabel}\n          <span\n            aria-hidden=\"true\"\n            className={cn(\n              'grid size-10 place-items-center rounded-full transition-transform duration-500 motion-reduce:transition-none',\n              SPRING_EASE,\n              sent\n                ? 'bg-[#0b3b3e] text-[#c6f56f]'\n                : 'bg-[#c6f56f] text-[#045f64] group-hover/trigger:scale-110'\n            )}\n          >\n            <svg\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              strokeWidth=\"2\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              className={cn(\n                'size-5 transition-transform duration-500 motion-reduce:transition-none',\n                SPRING_EASE,\n                !sent && 'group-hover/trigger:-translate-y-px group-hover/trigger:-rotate-12'\n              )}\n            >\n              {sent ? (\n                <path d=\"m5 12.5 4.5 4.5L19 7.5\" strokeWidth=\"2.4\" />\n              ) : (\n                <>\n                  <rect x=\"3\" y=\"5\" width=\"18\" height=\"14\" rx=\"2.5\" />\n                  <path d=\"m3.5 7 7.4 5.3a1.9 1.9 0 0 0 2.2 0L20.5 7\" />\n                </>\n              )}\n            </svg>\n          </span>\n        </button>\n\n        <div\n          ref={panelRef}\n          id={`${ids}-panel`}\n          role=\"dialog\"\n          aria-labelledby={`${ids}-title`}\n          className=\"invisible relative z-10 w-full opacity-0 will-change-transform [grid-area:1/1]\"\n        >\n          <div\n            ref={shadowRef}\n            aria-hidden=\"true\"\n            className=\"absolute inset-0 rounded-[28px] opacity-0 shadow-[0_40px_70px_-40px_#03110f]\"\n          />\n          <div\n            ref={surfaceRef}\n            aria-hidden=\"true\"\n            className=\"absolute inset-0 rounded-[28px] bg-[#0b3b3e] ring-1 ring-white/10 ring-inset\"\n          />\n          <form\n            ref={formRef}\n            noValidate\n            onSubmit={submit}\n            className=\"relative p-6 text-[#e8f3f2] md:p-7\"\n          >\n            <div className=\"flex items-center justify-between gap-4\">\n              <p id={`${ids}-title`} className=\"flex items-center gap-3 font-medium\">\n                <span aria-hidden=\"true\" className=\"size-2 rounded-full bg-[#c6f56f]\" />\n                {buttonLabel}\n              </p>\n              <button\n                type=\"button\"\n                aria-label=\"Close\"\n                onClick={() => close(true)}\n                className=\"group grid size-9 cursor-pointer place-items-center rounded-full text-[#9fd4d6] transition-colors duration-200 outline-none hover:bg-white/10 hover:text-white focus-visible:ring-2 focus-visible:ring-[#c6f56f] motion-reduce:transition-none\"\n              >\n                <svg\n                  viewBox=\"0 0 24 24\"\n                  aria-hidden=\"true\"\n                  className=\"size-4.5 transition-transform duration-700 ease-[cubic-bezier(0.16,1,0.3,1)] group-hover:rotate-360 motion-reduce:transition-none\"\n                >\n                  <path\n                    d=\"M6 6l12 12M18 6 6 18\"\n                    stroke=\"currentColor\"\n                    strokeWidth=\"2\"\n                    strokeLinecap=\"round\"\n                  />\n                </svg>\n              </button>\n            </div>\n\n            <p className=\"mt-7 flex flex-col gap-3 text-xl leading-snug md:text-2xl\">\n              <span>\n                Hi, I’m\n                <input\n                  ref={nameRef}\n                  value={message.name}\n                  onChange={(event) => update('name')(event.target.value)}\n                  aria-label=\"Your name\"\n                  aria-invalid={error?.field === 'name' || undefined}\n                  aria-describedby={describedBy('name')}\n                  autoComplete=\"name\"\n                  placeholder=\"your name\"\n                  size={fieldSize(message.name, 'your name')}\n                  className={FIELD_CLASS}\n                />\n                .\n              </span>\n              {topics.length > 0 && (\n                <>\n                  <span>I need help with</span>\n                  <TopicPicker\n                    name={`${ids}-topic`}\n                    topics={topics}\n                    value={topic}\n                    onChange={update('topic')}\n                  />\n                </>\n              )}\n              <span>\n                Reach me at\n                <input\n                  ref={emailRef}\n                  type=\"email\"\n                  value={message.email}\n                  onChange={(event) => update('email')(event.target.value)}\n                  aria-label=\"Your email\"\n                  aria-invalid={error?.field === 'email' || undefined}\n                  aria-describedby={describedBy('email')}\n                  autoComplete=\"email\"\n                  placeholder=\"you@company.com\"\n                  size={fieldSize(message.email, 'you@company.com')}\n                  className={FIELD_CLASS}\n                />\n                .\n              </span>\n            </p>\n\n            <label className=\"mt-6 block text-sm text-[#9fd4d6]\">\n              Anything we should know first? It’s optional.\n              <textarea\n                value={message.details}\n                onChange={(event) => update('details')(event.target.value)}\n                rows={2}\n                placeholder=\"Timeline, budget, a link to what you have now\"\n                className=\"mt-2 block w-full resize-none rounded-2xl border border-[#9fd4d6]/20 bg-white/5 px-4 py-3 text-base text-white transition-colors duration-200 outline-none placeholder:text-[#9fd4d6]/45 focus:border-[#c6f56f]/70 motion-reduce:transition-none\"\n              />\n            </label>\n\n            <p id={`${ids}-error`} role=\"alert\" className=\"mt-3 min-h-5 text-sm text-[#ffb4a8]\">\n              {error?.text}\n            </p>\n\n            <div className=\"mt-3 flex flex-wrap items-center justify-between gap-4\">\n              <p className=\"text-sm text-[#9fd4d6]\">{replyTime}</p>\n              <button\n                type=\"submit\"\n                disabled={status === 'sending'}\n                className=\"inline-flex h-12 cursor-pointer items-center rounded-full bg-[#c6f56f] px-6 font-medium text-[#0b3b3e] transition-transform duration-200 outline-none hover:scale-[1.03] focus-visible:ring-2 focus-visible:ring-white disabled:cursor-wait disabled:opacity-70 motion-reduce:transition-none motion-reduce:hover:scale-100\"\n              >\n                {status === 'sending' ? 'Sending…' : 'Send message'}\n              </button>\n            </div>\n          </form>\n        </div>\n      </div>\n\n      <p role=\"status\" className=\"sr-only\">\n        {sent ? `Message sent. ${replyTime}` : ''}\n      </p>\n    </section>\n  );\n};\n\nexport default CtaContactMorph;\n"
    }
  ],
  "docs": "Docs, props and examples: https://tween-ui.vercel.app/block/cta-contact-morph\nPass `onSend` to deliver the message (it receives `name`, `email`, `topic` and `details`); throw from it to show the retry message. Without it the block only simulates a send."
}