{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "footer-statement",
  "title": "Footer Statement",
  "description": "A closing footer with one large display line in an inverted tile, an email action and a quiet row of links.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "@manniche/data-tile"
  ],
  "files": [
    {
      "path": "registry/manniche/footer-statement/footer-statement.tsx",
      "content": "// A closing statement footer in the Tiles language. Signature: one very large display line in an inverted tile that\n// spans the width. The line sets itself to the box with fluid type (a clamp on the container's width) and never breaks\n// inside a word: it wraps between words, balanced, and the smallest size still fits the longest word at 280 px.\n// Under the tile sits one quiet row of links and the legal line. The tile has one action, an email address in mono and\n// a copy button that says \"Copied\" in a live region. Screen readers get a <footer> landmark, an h2 for the statement,\n// a <nav> list for the links and the copy result announced politely. The ©-year is computed after mount unless `now`\n// is passed. Under reduced motion nothing animates (the only motion is a short opacity/transform on press).\nimport { ArrowUpRight, Check, Copy } from 'lucide-react'\nimport { useEffect, useId, useRef, useState, useSyncExternalStore, type ComponentPropsWithoutRef } from 'react'\nimport { cn } from '@/lib/utils'\nimport { DataTile } from '@/registry/manniche/data-tile/data-tile'\n\nexport type FooterStatementLink = {\n  label: string\n  href: string\n  /** Opens in a new tab with rel=\"noopener noreferrer\" and a hidden \"(opens in a new tab)\". */\n  external?: boolean\n}\n\nexport type FooterStatementLabels = {\n  navigation?: string\n  copy?: string\n  copied?: string\n  failed?: string\n  emailLead?: string\n  newTab?: string\n}\n\nexport type FooterStatementProps = Omit<ComponentPropsWithoutRef<'footer'>, 'children'> & {\n  /** The statement, one sentence, e.g. \"Let's make the next one together.\" */\n  headline: string\n  /** The one primary action beside the email, e.g. \"Start a project\". */\n  action: FooterStatementLink\n  /** The email address shown in mono with a mailto link and a copy button. */\n  email?: string\n  /** A quiet row of links under the tile. */\n  links?: FooterStatementLink[]\n  /** The name after the © sign. */\n  owner: string\n  /** Legal links on the last line: privacy, terms, imprint. */\n  legalLinks?: FooterStatementLink[]\n  /** Pins the © year and keeps the first render pure. When absent the year appears after mount. */\n  now?: Date\n  /** UI strings, for translation. */\n  labels?: FooterStatementLabels\n}\n\nconst DEFAULT_LABELS: Required<FooterStatementLabels> = {\n  navigation: 'Footer',\n  copy: 'Copy',\n  copied: 'Copied',\n  failed: 'Not copied',\n  emailLead: 'Or write to',\n  newTab: '(opens in a new tab)',\n}\n\nconst noopSubscribe = () => () => {}\n// null on the server and in the first client render, the real year once mounted.\nfunction useYear(now?: Date) {\n  const clientYear = useSyncExternalStore(noopSubscribe, () => new Date().getFullYear(), () => null)\n  return now ? now.getFullYear() : clientYear\n}\n\nconst QUIET =\n  'relative inline-flex min-h-11 items-center rounded-md text-[13px] text-muted-foreground transition-[color] duration-200 ease-out-quint hover:text-foreground focus-visible:text-foreground focus-visible:outline-offset-0'\n\nfunction Anchor({ link, newTab, className }: { link: FooterStatementLink; newTab: string; className?: string }) {\n  return (\n    <a href={link.href} className={className} {...(link.external ? { target: '_blank', rel: 'noopener noreferrer' } : {})}>\n      {link.label}\n      {link.external && <span className=\"sr-only\"> {newTab}</span>}\n    </a>\n  )\n}\n\ntype CopyState = 'idle' | 'copied' | 'failed'\n\nfunction EmailCopy({ email, labels }: { email: string; labels: Required<FooterStatementLabels> }) {\n  const [state, setState] = useState<CopyState>('idle')\n  const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)\n  useEffect(() => () => clearTimeout(timer.current), [])\n\n  const copy = async () => {\n    try {\n      await navigator.clipboard.writeText(email)\n      setState('copied')\n    } catch {\n      setState('failed')\n    }\n    clearTimeout(timer.current)\n    timer.current = setTimeout(() => setState('idle'), 1800)\n  }\n\n  const text = { idle: labels.copy, copied: labels.copied, failed: labels.failed }[state]\n  const Icon = state === 'copied' ? Check : Copy\n  return (\n    <>\n      <button\n        type=\"button\"\n        onClick={copy}\n        className=\"inline-flex h-11 min-w-[5.75rem] cursor-pointer items-center justify-center gap-2 rounded-full border border-border px-4 text-[13px] font-medium text-foreground transition-[opacity,transform] duration-200 ease-out-quint hover:opacity-80 active:scale-[0.97] motion-reduce:transition-none\"\n      >\n        <Icon className=\"size-3.5\" aria-hidden />\n        {/* The visible word is the same text the live region announces. */}\n        <span aria-hidden>{text}</span>\n        <span className=\"sr-only\">\n          {labels.copy} {email}\n        </span>\n      </button>\n      <span role=\"status\" className=\"sr-only\">\n        {state === 'idle' ? '' : text}\n      </span>\n    </>\n  )\n}\n\n/** A closing statement in an inverted tile with one action, then a quiet row of links and the legal line. */\nexport function FooterStatement({\n  headline,\n  action,\n  email,\n  links = [],\n  owner,\n  legalLinks = [],\n  now,\n  labels,\n  className,\n  ...rest\n}: FooterStatementProps) {\n  const l = { ...DEFAULT_LABELS, ...labels }\n  const year = useYear(now)\n  const headingId = useId()\n\n  return (\n    <footer {...rest} aria-labelledby={headingId} className={cn('@container w-full bg-background text-foreground', className)}>\n      <div className=\"mx-auto w-full max-w-6xl px-4 py-12 @min-[40rem]:py-16 sm:px-6\">\n        <DataTile inverted>\n          <div className=\"flex flex-col gap-10 py-4 @min-[40rem]:gap-14 @min-[40rem]:px-4 @min-[40rem]:py-8\">\n            <h2\n              id={headingId}\n              // Fluid: 11 % of the box's width, never below 36 px (the longest word still fits at 280 px) nor above 128 px.\n              className=\"max-w-[16ch] leading-[0.98] font-extrabold tracking-[-0.045em] text-balance [hyphens:none] [overflow-wrap:normal] [word-break:normal]\"\n              style={{ fontFamily: 'var(--font-display, inherit)', fontStretch: '86%', fontSize: 'clamp(2.25rem, 11cqi, 8rem)' }}\n            >\n              {headline}\n            </h2>\n            <div className=\"flex flex-wrap items-center gap-x-6 gap-y-4\">\n              <a\n                href={action.href}\n                {...(action.external ? { target: '_blank', rel: 'noopener noreferrer' } : {})}\n                className=\"group/action inline-flex h-12 items-center gap-2 rounded-full bg-foreground px-6 text-[15px] font-medium text-background transition-[opacity,transform] duration-200 ease-out-quint hover:opacity-90 active:scale-[0.97] motion-reduce:transition-none\"\n              >\n                {action.label}\n                {action.external && <span className=\"sr-only\"> {l.newTab}</span>}\n                <ArrowUpRight className=\"size-4\" aria-hidden />\n              </a>\n              {email && (\n                <div className=\"flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1\">\n                  <span className=\"text-sm text-muted-foreground\">{l.emailLead}</span>\n                  <a\n                    href={`mailto:${email}`}\n                    className=\"inline-flex min-h-11 items-center rounded-md font-mono text-sm break-all underline decoration-border underline-offset-4 transition-[opacity] duration-200 hover:opacity-80\"\n                  >\n                    {email}\n                  </a>\n                  <EmailCopy email={email} labels={l} />\n                </div>\n              )}\n            </div>\n          </div>\n        </DataTile>\n\n        <div className=\"mt-4 flex flex-col gap-x-8 gap-y-1 px-1 @min-[40rem]:flex-row @min-[40rem]:items-center @min-[40rem]:justify-between @min-[40rem]:px-2\">\n          {links.length > 0 && (\n            <nav aria-label={l.navigation}>\n              <ul className=\"flex flex-wrap gap-x-6\">\n                {links.map((link) => (\n                  <li key={link.href + link.label}>\n                    <Anchor link={link} newTab={l.newTab} className={QUIET} />\n                  </li>\n                ))}\n              </ul>\n            </nav>\n          )}\n          <div className=\"flex flex-wrap items-center gap-x-5\">\n            <p className=\"min-h-11 content-center font-mono text-xs text-muted-foreground tabular-nums\">\n              &copy; {year ?? ''}\n              {year ? ' ' : ''}\n              {owner}\n            </p>\n            {legalLinks.length > 0 && (\n              <ul className=\"flex flex-wrap gap-x-5\">\n                {legalLinks.map((link) => (\n                  <li key={link.href + link.label}>\n                    <Anchor link={link} newTab={l.newTab} className={QUIET} />\n                  </li>\n                ))}\n              </ul>\n            )}\n          </div>\n        </div>\n      </div>\n    </footer>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "categories": [
    "templates"
  ],
  "type": "registry:block"
}