{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"newsletter-footer","type":"registry:component","title":"Newsletter footer","description":"A footer that’s still alive: a signup that becomes its own confirmation, drawn link underlines and your name in outline.","author":"Hairline UI","dependencies":[],"registryDependencies":["https://hairlineui.com/r/number-roll.json","https://hairlineui.com/r/text-morph.json"],"files":[{"path":"registry/newsletter-footer.tsx","type":"registry:component","content":"\"use client\";\n\nimport * as React from \"react\";\nimport { NumberRoll } from \"./number-roll\";\nimport { TextMorph } from \"./text-morph\";\n\n/* ─────────────────────────────────────────────────────────\n * NEWSLETTER FOOTER: the last thing on the page, still alive\n *\n *   signup     an email field with its button inside; a likely\n *              typo (\"gmial.com\") is caught as you type and\n *              fixed in one click\n *   sending    the button opens a spinner, \"Subscribing\"\n *   sent       the field blurs into \"Check your inbox\" with a\n *              check drawing itself; the button becomes\n *              \"Resend in 0:30\", the seconds rolling down;\n *              \"Wrong address?\" morphs it back, email selected\n *   links      columns whose underlines draw in from the left\n *              and leave to the right\n *   wordmark   your name across the full width in 1px outline;\n *              letters rise in, and fill where the pointer\n *              passes, fading out behind it\n * ───────────────────────────────────────────────────────── */\n\nexport interface FooterLink {\n  label: string;\n  href?: string;\n  onClick?: () => void;\n  /** A small tag beside the link, e.g. \"New\" or \"Hiring\". */\n  badge?: string;\n}\n\nexport interface FooterColumn {\n  title: string;\n  links: FooterLink[];\n}\n\nexport interface FooterSocial {\n  label: string;\n  href: string;\n  icon: React.ReactNode;\n}\n\nexport interface FooterNewsletter {\n  title?: string;\n  description?: string;\n  placeholder?: string;\n  buttonLabel?: string;\n  /** Subscribe the address; throw an Error to show its message. */\n  onSubscribe: (email: string) => Promise<unknown> | unknown;\n  /** Seconds before the email can be sent again. */\n  resendAfter?: number;\n}\n\nexport interface NewsletterFooterProps extends React.HTMLAttributes<HTMLElement> {\n  brand: { name: string; logo?: React.ReactNode; href?: string; tagline?: string };\n  columns?: FooterColumn[];\n  newsletter?: FooterNewsletter;\n  socials?: FooterSocial[];\n  /** Small links in the bottom row: privacy, terms, cookie settings. */\n  legal?: FooterLink[];\n  copyright?: React.ReactNode;\n  /** A link to your status page, with a live dot. */\n  status?: { state: \"operational\" | \"degraded\" | \"outage\"; label?: string; href?: string };\n  /** The name in outline across the full width; a string to use other words. */\n  wordmark?: boolean | string;\n}\n\nconst EASE = \"cubic-bezier(0.16,1,0.3,1)\";\nconst FOCUS = \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40 focus-visible:ring-offset-2 focus-visible:ring-offset-background\";\nconst LINK = `rounded-sm text-muted-foreground transition-[color,background-size] duration-300 ease-[cubic-bezier(0.16,1,0.3,1)] [background:linear-gradient(currentColor,currentColor)_no-repeat_100%_100%/0%_1px] hover:text-foreground hover:[background-position:0_100%] hover:[background-size:100%_1px] motion-reduce:transition-none ${FOCUS}`;\nconst EMAIL = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]{2,}$/;\nconst STATUS = {\n  operational: { label: \"All systems normal\", dot: \"bg-emerald-500\" },\n  degraded: { label: \"Some systems slow\", dot: \"bg-amber-500\" },\n  outage: { label: \"Outage in progress\", dot: \"bg-red-500\" },\n};\n\nconst reducedQuery = \"(prefers-reduced-motion: reduce)\";\nconst subscribeReduced = (onChange: () => void) => {\n  const query = window.matchMedia(reducedQuery);\n  query.addEventListener(\"change\", onChange);\n  return () => query.removeEventListener(\"change\", onChange);\n};\nconst useReducedMotion = () =>\n  React.useSyncExternalStore(subscribeReduced, () => window.matchMedia(reducedQuery).matches, () => false);\n\n/* Common mail domains, to catch \"gmial.com\" before it bounces. */\nconst DOMAINS = [\"gmail.com\", \"googlemail.com\", \"outlook.com\", \"hotmail.com\", \"live.com\", \"yahoo.com\", \"icloud.com\", \"me.com\", \"mac.com\", \"proton.me\", \"protonmail.com\", \"aol.com\", \"mail.com\", \"gmx.com\", \"fastmail.com\", \"hey.com\", \"yandex.com\"];\n\nfunction distance(a: string, b: string) {\n  const d = Array.from({ length: a.length + 1 }, (_, i) => Array.from({ length: b.length + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)));\n  for (let i = 1; i <= a.length; i++)\n    for (let j = 1; j <= b.length; j++) {\n      d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));\n      if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);\n    }\n  return d[a.length][b.length];\n}\n\nfunction suggest(email: string) {\n  const at = email.lastIndexOf(\"@\");\n  if (at < 1) return null;\n  const domain = email.slice(at + 1).toLowerCase();\n  if (!/\\.[a-z]{2,}$/.test(domain) || DOMAINS.includes(domain)) return null;\n  let best = \"\";\n  let score = Infinity;\n  for (const known of DOMAINS) {\n    const s = distance(domain, known);\n    if (s < score) [best, score] = [known, s];\n  }\n  return score <= (domain.length > 6 ? 2 : 1) ? email.slice(0, at + 1) + best : null;\n}\n\nfunction FooterAnchor({ link, className = \"\" }: { link: FooterLink; className?: string }) {\n  const content = (\n    <>\n      {link.label}\n      {link.badge && <span className=\"ml-2 rounded-full px-1.5 py-px text-[10.5px] font-medium text-foreground shadow-[inset_0_0_0_1px_var(--border)]\">{link.badge}</span>}\n    </>\n  );\n  // The badge sits outside the drawn underline.\n  return link.href ? (\n    <a href={link.href} onClick={link.onClick} className={`${LINK} ${className}`}>\n      {content}\n    </a>\n  ) : (\n    <button type=\"button\" onClick={link.onClick} className={`${LINK} ${className}`}>\n      {content}\n    </button>\n  );\n}\n\nfunction Signup({ title = \"Get the changelog by email\", description = \"One email a month, when something ships. No spam.\", placeholder = \"you@company.com\", buttonLabel = \"Subscribe\", onSubscribe, resendAfter = 30, reduced }: FooterNewsletter & { reduced: boolean }) {\n  const id = React.useId();\n  const [email, setEmail] = React.useState(\"\");\n  const [phase, setPhase] = React.useState<\"idle\" | \"sending\" | \"sent\" | \"resending\">(\"idle\");\n  const [error, setError] = React.useState<string | null>(null);\n  const [sentTo, setSentTo] = React.useState(\"\");\n  const [seconds, setSeconds] = React.useState(0);\n  const [resent, setResent] = React.useState(false);\n  const boxRef = React.useRef<HTMLDivElement>(null);\n  const inputRef = React.useRef<HTMLInputElement>(null);\n  const buttonRef = React.useRef<HTMLButtonElement>(null);\n  const flash = React.useRef(0);\n\n  const sent = phase === \"sent\" || phase === \"resending\";\n  const busy = phase === \"sending\" || phase === \"resending\";\n  const counting = phase === \"sent\" && seconds > 0 && !resent;\n  const fix = phase === \"idle\" && !error ? suggest(email.trim()) : null;\n\n  // The resend countdown.\n  React.useEffect(() => {\n    if (!sent || seconds <= 0) return;\n    const timer = window.setTimeout(() => setSeconds((s) => s - 1), 1000);\n    return () => window.clearTimeout(timer);\n  }, [sent, seconds]);\n  React.useEffect(() => () => window.clearTimeout(flash.current), []);\n\n  // The helper's action keeps its label while it folds away.\n  const action = sent ? { kind: \"wrong\", label: \"Wrong address?\" } : fix ? { kind: \"fix\", label: `${fix}?` } : null;\n  const [kept, setKept] = React.useState(action);\n  if (action && (action.kind !== kept?.kind || action.label !== kept?.label)) setKept(action);\n\n  const reason = (err: unknown) => (err instanceof Error && err.message ? err.message : \"That didn’t go through. Try again?\");\n\n  const submit = async (e: React.FormEvent) => {\n    e.preventDefault();\n    if (busy || sent) return;\n    const value = email.trim();\n    if (!EMAIL.test(value)) {\n      setError(value ? \"That doesn’t look like an email address.\" : \"Add your email address first.\");\n      if (!reduced)\n        boxRef.current?.animate(\n          [{ transform: \"none\" }, { transform: \"translateX(-6px)\" }, { transform: \"translateX(5px)\" }, { transform: \"translateX(-3px)\" }, { transform: \"translateX(2px)\" }, { transform: \"none\" }],\n          { duration: 380, easing: \"ease-out\" },\n        );\n      inputRef.current?.focus();\n      return;\n    }\n    const typed = document.activeElement === inputRef.current;\n    setError(null);\n    setPhase(\"sending\");\n    try {\n      await onSubscribe(value);\n      setSentTo(value);\n      setSeconds(resendAfter);\n      setPhase(\"sent\");\n      if (typed) requestAnimationFrame(() => buttonRef.current?.focus({ preventScroll: true }));\n    } catch (err) {\n      setError(reason(err));\n      setPhase(\"idle\");\n    }\n  };\n\n  const resend = async () => {\n    if (phase !== \"sent\" || seconds > 0) return;\n    setError(null);\n    setPhase(\"resending\");\n    try {\n      await onSubscribe(sentTo);\n      setResent(true);\n      window.clearTimeout(flash.current);\n      flash.current = window.setTimeout(() => setResent(false), 2200);\n      setSeconds(resendAfter);\n    } catch (err) {\n      setError(reason(err));\n    }\n    setPhase(\"sent\");\n  };\n\n  const wrong = () => {\n    setPhase(\"idle\");\n    setError(null);\n    setResent(false);\n    requestAnimationFrame(() => {\n      inputRef.current?.focus();\n      inputRef.current?.select();\n    });\n  };\n\n  const helper = sent\n    ? error ?? (resent ? `Sent again to ${sentTo}.` : `We sent a link to ${sentTo}.`)\n    : error ?? (fix ? \"Did you mean\" : description);\n  const label = phase === \"sending\" ? \"Subscribing\" : phase === \"resending\" ? \"Sending\" : sent ? (resent ? \"Sent\" : seconds > 0 ? \"Resend in\" : \"Resend\") : buttonLabel;\n  const swap = (on: boolean, delay = 0) =>\n    reduced\n      ? { opacity: on ? 1 : 0 }\n      : {\n          opacity: on ? 1 : 0,\n          filter: on ? \"none\" : \"blur(4px)\",\n          transform: on ? \"none\" : \"translateY(5px)\",\n          transition: `opacity 320ms ${EASE} ${on ? delay : 0}ms, filter 320ms ${EASE} ${on ? delay : 0}ms, transform 420ms ${EASE} ${on ? delay : 0}ms`,\n        };\n\n  return (\n    <form onSubmit={submit} noValidate className=\"w-full max-w-[420px]\">\n      <p className=\"text-[14px] font-medium text-foreground\">{title}</p>\n      <label htmlFor={`${id}-email`} className=\"sr-only\">\n        Email address\n      </label>\n      {/* One surface: the field, then the confirmation, in the same box. */}\n      <div\n        ref={boxRef}\n        className=\"mt-3 flex h-12 items-center rounded-full bg-background p-1.5 has-[input:focus-visible]:ring-2 has-[input:focus-visible]:ring-ring/30\"\n        style={{ boxShadow: `inset 0 0 0 1px ${error && !sent ? \"rgb(239 68 68 / 0.6)\" : \"var(--border)\"}`, transition: \"box-shadow 200ms\" }}\n      >\n        <div className=\"relative min-w-0 flex-1 self-stretch\">\n          <input\n            ref={inputRef}\n            id={`${id}-email`}\n            type=\"email\"\n            name=\"email\"\n            inputMode=\"email\"\n            autoComplete=\"email\"\n            spellCheck={false}\n            placeholder={placeholder}\n            value={email}\n            readOnly={busy}\n            inert={sent}\n            aria-invalid={(error !== null && !sent) || undefined}\n            aria-describedby={`${id}-help`}\n            onChange={(e) => {\n              setEmail(e.target.value);\n              if (error) setError(null);\n            }}\n            className=\"absolute inset-0 w-full bg-transparent pl-3 pr-2 text-[14px] text-foreground outline-none placeholder:text-muted-foreground\"\n            style={swap(!sent, 120)}\n          />\n          <div aria-hidden={!sent || undefined} className=\"pointer-events-none absolute inset-0 flex items-center gap-2 pl-2.5 text-[14px] font-medium text-foreground\" style={swap(sent, 160)}>\n            <svg aria-hidden=\"true\" viewBox=\"0 0 20 20\" fill=\"none\" className=\"size-[18px] shrink-0 text-emerald-600 dark:text-emerald-400\">\n              <circle cx=\"10\" cy=\"10\" r=\"8.25\" stroke=\"currentColor\" strokeOpacity=\"0.35\" strokeWidth=\"1\" />\n              <path\n                d=\"M6.5 10.4 8.8 12.7 13.6 7.6\"\n                stroke=\"currentColor\"\n                strokeWidth=\"1.6\"\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n                pathLength={1}\n                strokeDasharray={1}\n                style={{ strokeDashoffset: sent ? 0 : 1, transition: sent && !reduced ? `stroke-dashoffset 420ms ${EASE} 320ms` : \"none\" }}\n              />\n            </svg>\n            <span className=\"truncate\">Check your inbox</span>\n          </div>\n        </div>\n        <button\n          ref={buttonRef}\n          type={sent ? \"button\" : \"submit\"}\n          onClick={sent ? resend : undefined}\n          aria-disabled={busy || counting || undefined}\n          aria-busy={busy || undefined}\n          className={`inline-flex h-9 shrink-0 items-center rounded-full text-[13px] font-medium ${sent ? \"px-3.5\" : \"px-4\"} transition-[background-color,color,box-shadow] duration-300 ${FOCUS} ${\n            sent\n              ? `bg-transparent shadow-[inset_0_0_0_1px_var(--border)] ${counting || busy ? \"text-muted-foreground\" : \"text-foreground hover:bg-accent\"}`\n              : \"bg-primary text-primary-foreground hover:bg-primary/90\"\n          }`}\n        >\n          <span\n            aria-hidden=\"true\"\n            className=\"relative flex h-3.5 shrink-0 items-center justify-center\"\n            style={{ width: busy || resent ? 14 : 0, marginRight: busy || resent ? 6 : 0, transition: reduced ? \"none\" : `width 320ms ${EASE}, margin 320ms ${EASE}` }}\n          >\n            <span\n              className={`absolute size-3 rounded-full border-[1.5px] border-current/30 border-t-current motion-reduce:animate-none ${busy ? \"animate-spin\" : \"\"}`}\n              style={{ opacity: busy ? 1 : 0, transition: reduced ? \"none\" : \"opacity 200ms\" }}\n            />\n            <svg viewBox=\"0 0 16 16\" fill=\"none\" className=\"absolute size-3.5\" style={{ opacity: resent ? 1 : 0 }}>\n              <path\n                d=\"M3.5 8.5 6.5 11.5 12.5 4.5\"\n                stroke=\"currentColor\"\n                strokeWidth=\"2\"\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n                pathLength={1}\n                strokeDasharray={1}\n                style={{ strokeDashoffset: resent ? 0 : 1, transition: resent && !reduced ? `stroke-dashoffset 380ms ${EASE} 120ms` : \"none\" }}\n              />\n            </svg>\n          </span>\n          <TextMorph>{label}</TextMorph>\n          {/* The countdown opens after \"Resend in\", the seconds rolling down. */}\n          <span\n            aria-hidden={!counting || undefined}\n            className=\"inline-grid\"\n            style={{ gridTemplateColumns: counting ? \"1fr\" : \"0fr\", opacity: counting ? 1 : 0, transition: reduced ? \"none\" : `grid-template-columns 420ms ${EASE}, opacity 300ms ${EASE}` }}\n          >\n            <span className=\"min-w-0 overflow-hidden\">\n              <span className=\"inline-flex whitespace-nowrap pl-1 tabular-nums\">\n                <NumberRoll value={Math.floor(seconds / 60)} direction=\"down\" />:\n                <NumberRoll value={seconds % 60} format={{ minimumIntegerDigits: 2 }} direction=\"down\" />\n              </span>\n            </span>\n          </span>\n        </button>\n      </div>\n      <p id={`${id}-help`} aria-live=\"polite\" className={`mt-2.5 min-h-5 px-1 text-[12.5px] leading-5 transition-colors duration-200 ${error ? \"text-red-600 dark:text-red-400\" : \"text-muted-foreground\"}`}>\n        <TextMorph animateWidth={false}>{helper}</TextMorph>\n        <span\n          className=\"inline-grid align-top\"\n          style={{ gridTemplateColumns: action ? \"1fr\" : \"0fr\", opacity: action ? 1 : 0, transition: reduced ? \"none\" : `grid-template-columns 420ms ${EASE}, opacity 260ms ${EASE}` }}\n        >\n          <span className=\"min-w-0\">\n            <button\n              type=\"button\"\n              inert={!action}\n              onClick={() => (kept?.kind === \"wrong\" ? wrong() : fix && setEmail(fix))}\n              className={`ml-1 whitespace-nowrap rounded-sm font-medium text-foreground underline decoration-border underline-offset-4 transition-colors hover:decoration-current ${FOCUS}`}\n            >\n              <TextMorph>{kept?.label ?? \"\"}</TextMorph>\n            </button>\n          </span>\n        </span>\n      </p>\n    </form>\n  );\n}\n\n/**\n * Your name across the full width in 1px outline, sitting on the footer's bottom edge so\n * descenders sink into it; letters rise out of that edge and fill where the pointer passes.\n */\nfunction Wordmark({ text, reduced }: { text: string; reduced: boolean }) {\n  const boxRef = React.useRef<HTMLDivElement>(null);\n  const wordRef = React.useRef<HTMLSpanElement>(null);\n  const probeRef = React.useRef<HTMLSpanElement>(null);\n  const [size, setSize] = React.useState(120);\n  const [baseline, setBaseline] = React.useState(0.86);\n  const [shown, setShown] = React.useState(false);\n\n  // Sized so the word spans the footer exactly, at any width and in any font.\n  React.useLayoutEffect(() => {\n    const box = boxRef.current;\n    const word = wordRef.current;\n    if (!box || !word) return;\n    const fit = () => {\n      const current = parseFloat(getComputedStyle(word).fontSize);\n      if (!box.clientWidth || !word.offsetWidth || !current) return;\n      const next = (current * box.clientWidth) / word.offsetWidth;\n      setSize((s) => (Math.abs(s - next) < 0.5 ? s : next));\n      // Where the baseline falls, as a share of the font size, so the box can end just below it.\n      const probe = probeRef.current;\n      if (probe) setBaseline((probe.getBoundingClientRect().top - word.getBoundingClientRect().top) / current);\n    };\n    fit();\n    const observer = new ResizeObserver(fit);\n    observer.observe(box);\n    document.fonts?.ready.then(fit);\n    return () => observer.disconnect();\n  }, [text]);\n\n  React.useEffect(() => {\n    const box = boxRef.current;\n    if (!box) return;\n    const observer = new IntersectionObserver(\n      ([entry]) => {\n        if (!entry.isIntersecting) return;\n        setShown(true);\n        observer.disconnect();\n      },\n      { threshold: 0.3 },\n    );\n    observer.observe(box);\n    return () => observer.disconnect();\n  }, []);\n\n  return (\n    <div ref={boxRef} aria-hidden=\"true\" className=\"mt-10 select-none overflow-hidden\" style={{ fontSize: size, height: \"0.83em\" }}>\n      {/* The right padding gives back the last letter's negative tracking, so it isn't clipped. */}\n      <span ref={wordRef} className=\"block w-max whitespace-nowrap pr-[0.06em] font-medium leading-none tracking-[-0.055em]\" style={{ marginTop: `${0.8 - baseline}em` }}>\n        {[...text].map((ch, i) => (\n          <span\n            key={i}\n            className=\"inline-block\"\n            style={{ transform: shown || reduced ? \"none\" : \"translateY(115%)\", transition: reduced ? \"none\" : `transform 1000ms ${EASE} ${i * 60}ms` }}\n          >\n            <span className=\"inline-block text-transparent transition-colors duration-[1600ms] ease-out [-webkit-text-stroke:1px_color-mix(in_oklab,var(--foreground)_30%,transparent)] hover:text-foreground hover:duration-0\">\n              {ch === \" \" ? \" \" : ch}\n            </span>\n          </span>\n        ))}\n        <span ref={probeRef} className=\"inline-block h-0 w-0\" />\n      </span>\n    </div>\n  );\n}\n\nexport function NewsletterFooter({ brand, columns = [], newsletter, socials = [], legal = [], copyright, status, wordmark = true, className = \"\", ...props }: NewsletterFooterProps) {\n  const reduced = useReducedMotion();\n  const word = wordmark === true ? brand.name : wordmark || null;\n  const live = status ? STATUS[status.state] : null;\n  const year = new Date().getFullYear();\n\n  return (\n    <footer className={`w-full border-t border-border bg-background text-foreground ${className}`} {...props}>\n      <div className=\"mx-auto w-full max-w-6xl px-6 pt-14 sm:pt-16\">\n        <div className=\"grid gap-12 lg:grid-cols-[minmax(0,1fr)_minmax(0,1.25fr)]\">\n          <div>\n            <a href={brand.href ?? \"/\"} className={`inline-flex items-center gap-2.5 rounded-md text-[15px] font-medium tracking-[-0.01em] ${FOCUS}`}>\n              {brand.logo}\n              {brand.name}\n            </a>\n            {brand.tagline && <p className=\"mt-2 max-w-[36ch] text-[13px] leading-relaxed text-muted-foreground\">{brand.tagline}</p>}\n            {newsletter && (\n              <div className=\"mt-8\">\n                <Signup {...newsletter} reduced={reduced} />\n              </div>\n            )}\n          </div>\n          {columns.length > 0 && (\n            <nav aria-label=\"Footer\" className=\"grid grid-cols-2 gap-x-8 gap-y-10 sm:grid-cols-3\">\n              {columns.map((column) => (\n                <div key={column.title}>\n                  <p className=\"text-[12.5px] font-medium text-foreground\">{column.title}</p>\n                  <ul className=\"mt-4 space-y-2.5 text-[13px]\">\n                    {column.links.map((link) => (\n                      <li key={link.label}>\n                        <FooterAnchor link={link} />\n                      </li>\n                    ))}\n                  </ul>\n                </div>\n              ))}\n            </nav>\n          )}\n        </div>\n\n        <div className=\"mt-14 flex flex-wrap items-center justify-between gap-x-6 gap-y-4 border-t border-border pt-6 text-[12.5px] text-muted-foreground\">\n          <div className=\"flex flex-wrap items-center gap-x-5 gap-y-2\">\n            <span>{copyright ?? `© ${year} ${brand.name}`}</span>\n            {legal.map((link) => (\n              <FooterAnchor key={link.label} link={link} />\n            ))}\n          </div>\n          <div className=\"flex flex-wrap items-center gap-3\">\n            {live && status && (\n              <a\n                href={status.href}\n                className={`inline-flex h-7 shrink-0 items-center gap-2 whitespace-nowrap rounded-full px-3 text-foreground shadow-[inset_0_0_0_1px_var(--border)] transition-colors hover:bg-accent ${FOCUS}`}\n              >\n                <span aria-hidden=\"true\" className={`size-2 rounded-full transition-colors duration-500 motion-safe:animate-[ui-breathe_2.4s_ease-in-out_infinite] ${live.dot}`} />\n                <TextMorph>{status.label ?? live.label}</TextMorph>\n              </a>\n            )}\n            {socials.length > 0 && (\n              <ul className=\"flex items-center gap-0.5\">\n                {socials.map((social) => (\n                  <li key={social.label}>\n                    <a\n                      href={social.href}\n                      aria-label={social.label}\n                      className={`group grid size-8 place-items-center rounded-full text-muted-foreground transition-colors hover:bg-accent hover:text-foreground ${FOCUS} [&_svg]:size-4`}\n                    >\n                      <span className=\"transition-transform duration-300 ease-[cubic-bezier(0.34,1.36,0.64,1)] group-hover:-translate-y-0.5 motion-reduce:transition-none\">{social.icon}</span>\n                    </a>\n                  </li>\n                ))}\n              </ul>\n            )}\n          </div>\n        </div>\n\n        {word ? <Wordmark text={word} reduced={reduced} /> : <div className=\"h-8\" />}\n      </div>\n    </footer>\n  );\n}\n"}],"css":{"@keyframes ui-breathe":{"0%, 100%":{"transform":"scale(0.82)","opacity":"0.55"},"50%":{"transform":"scale(1)","opacity":"1"}}},"docs":"Docs and live preview: https://hairlineui.com/ui/newsletter-footer"}