{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "newsletter-form",
  "type": "registry:ui",
  "title": "Newsletter Form",
  "description": "Canonical email-signup pattern: title + (optional) description + email field + consent checkbox + submit. Composition of the auth primitive set (Form / Field / Input / Button + Checkbox + Typography). The block owns *structure*; consumers wire `action` and…",
  "author": "ofri-peretz <https://github.com/ofri-peretz>",
  "categories": [
    "marketing",
    "pattern"
  ],
  "dependencies": [],
  "registryDependencies": [
    "https://ds.interlace.tools/r/theme.json",
    "https://ds.interlace.tools/r/button.json",
    "https://ds.interlace.tools/r/checkbox.json",
    "https://ds.interlace.tools/r/form.json",
    "https://ds.interlace.tools/r/input.json",
    "https://ds.interlace.tools/r/skeleton.json",
    "https://ds.interlace.tools/r/stack.json",
    "https://ds.interlace.tools/r/typography.json"
  ],
  "files": [
    {
      "path": "registry/interlace-ui/patterns/newsletter-form.tsx",
      "target": "components/ui/patterns/newsletter-form.tsx",
      "type": "registry:ui",
      "content": "import * as React from 'react';\n\n// @interlace/newsletter-form v1.2.0 — Interlace design system.\n// Docs, props and live preview: https://ds.interlace.tools/c/newsletter-form\n// What changed since: https://ds.interlace.tools/c/newsletter-form#history\n// Generated banner — keep it, the upgrade diff reads this version.\n\n/**\n * @interlace/ui — NewsletterForm\n *\n * Canonical email-signup pattern: title + (optional) description + email\n * field + consent checkbox + submit. Composition of the auth primitive set\n * (Form / Field / Input / Button + Checkbox + Typography). The block owns\n * *structure*; consumers wire `action` and pass brand copy via props.\n *\n * Includes a honeypot `<input type=\"text\" name=\"website\">` rendered with\n * `tabIndex={-1}`, `aria-hidden`, and `.sr-only` — invisible + unreachable\n * to humans + assistive tech, but auto-filled by naive form-spam bots. The\n * server handler should reject any submission where `website` is non-empty.\n *\n * ## Anatomy\n *\n *   NewsletterForm                    (block — data-min-viewport=320)\n *     ├─ Typography variant=h3        (title)\n *     ├─ Typography variant=body      (description — optional)\n *     ├─ Form                         (the <form> element from primitives/form.tsx)\n *     │   ├─ Field name=\"email\"\n *     │   │   ├─ FieldLabel\n *     │   │   └─ FieldControl render={<Input type=\"email\" />}\n *     │   ├─ <label> Checkbox + consentLabel\n *     │   ├─ <input type=\"text\" name=\"website\" /> (honeypot — sr-only, aria-hidden)\n *     │   └─ Button type=\"submit\"\n *     └─ <slot for footer>\n *\n * ## MIN_VIEWPORT — 320\n *\n * Newsletter forms are typically embedded in footers and sidebars on the\n * narrowest viewports; must work on a 320 CSS-px iPhone SE.\n *\n * | Rule | Concept                          | Where in this file                                          |\n * | ---- | -------------------------------- | ----------------------------------------------------------- |\n * | R4   | Extends React.ComponentProps     | `React.ComponentProps<'form'> & NewsletterFormProps`        |\n * | R6   | data-slot on root                | `data-slot=\"newsletter-form\"`                               |\n * | R7   | className merged + ...rest       | `cn(...) + {...props}` on the Form root                     |\n * | R10  | Composition seam                 | `footer` prop slots consumer-supplied UI                    |\n * | R13  | Ecosystem first                  | Built on the primitives layer; no bespoke form state        |\n * | R14  | Declares min viewport            | `data-min-viewport={String(MIN_VIEWPORT)}` + exported const |\n * | R18  | Tailwind only                    | Zero inline style; classes only                             |\n * | R19  | Tokens only                      | Spacing/typography from existing primitives                 |\n * | R25  | Server component                 | No hooks → no `'use client'`                                |\n * | R26  | A11y from primitives             | Base UI Field owns label association + aria-describedby     |\n */\n\nimport { Button } from '@/components/ui/button';\nimport { Checkbox } from '@/components/ui/checkbox';\nimport { Field, FieldControl, FieldError, FieldLabel, Form } from '@/components/ui/form';\nimport { Input } from '@/components/ui/input';\nimport { Skeleton } from '@/components/ui/skeleton';\nimport { Stack } from '@/components/ui/stack';\nimport { Typography } from '@/components/ui/typography';\n\nexport const MIN_VIEWPORT = 320 as const;\n\ntype NewsletterFormProps = React.ComponentProps<'form'> & {\n  /** Headline above the form. Defaults to \"Subscribe to the newsletter\". */\n  title?: React.ReactNode;\n  /** Optional supporting copy under the title. */\n  description?: React.ReactNode;\n  /** Label on the submit button. Defaults to \"Subscribe\". */\n  submitLabel?: React.ReactNode;\n  /** Consent text rendered next to the consent checkbox. */\n  consentLabel?: React.ReactNode;\n  /** Slot below the form — typically a privacy-policy link or unsubscribe note. */\n  footer?: React.ReactNode;\n  /**\n   * When true, render a `<Skeleton variant=\"newsletter-form\" />` placeholder\n   * (title + input + submit silhouette). Useful when the form's i18n\n   * strings stream in from a CMS.\n   */\n  loading?: boolean;\n};\n\nexport function NewsletterForm({\n  className,\n  title = 'Subscribe to the newsletter',\n  description,\n  submitLabel = 'Subscribe',\n  consentLabel = 'I agree to receive occasional emails. Unsubscribe any time.',\n  footer,\n  loading,\n  ...props\n}: NewsletterFormProps) {\n  if (loading) {\n    return (\n      <Skeleton\n        variant=\"newsletter-form\"\n        data-slot=\"newsletter-form\"\n        className={className}\n      />\n    );\n  }\n  return (\n    <Stack\n      gap=\"lg\"\n      data-slot=\"newsletter-form\"\n      data-min-viewport={String(MIN_VIEWPORT)}\n      className=\"w-full max-w-96\"\n    >\n      <div>\n        <Typography as=\"h3\" variant=\"h3\">\n          {title}\n        </Typography>\n        {description ? (\n          <Typography variant=\"body\" tone=\"muted\" className=\"mt-2\">\n            {description}\n          </Typography>\n        ) : null}\n      </div>\n\n      <Form className={className} {...props}>\n        <Stack gap=\"md\">\n          <Field name=\"email\">\n            <FieldLabel>Email</FieldLabel>\n            <FieldControl\n              render={\n                <Input\n                  type=\"email\"\n                  autoComplete=\"email\"\n                  placeholder=\"you@interlace.tools\"\n                  required\n                />\n              }\n            />\n            <FieldError />\n          </Field>\n\n          <label className=\"flex items-start gap-sm text-ui-sm\">\n            <Checkbox name=\"consent\" required className=\"mt-0.5\" />\n            <span className=\"text-muted-foreground\">{consentLabel}</span>\n          </label>\n\n          {/*\n            Honeypot field. Hidden from sighted users (sr-only) and AT\n            (aria-hidden + tabIndex=-1); naive bots will still fill it.\n            The server handler should reject any submission where this\n            field is non-empty.\n          */}\n          <input\n            type=\"text\"\n            name=\"website\"\n            tabIndex={-1}\n            aria-hidden\n            autoComplete=\"off\"\n            className=\"sr-only\"\n          />\n\n          <Button type=\"submit\" className=\"mt-2 w-full\">\n            {submitLabel}\n          </Button>\n        </Stack>\n      </Form>\n\n      {footer ? (\n        <Typography variant=\"ui-sm\" tone=\"muted\" align=\"center\">\n          {footer}\n        </Typography>\n      ) : null}\n    </Stack>\n  );\n}\n"
    }
  ],
  "meta": {
    "tier": "pattern",
    "client": false,
    "minViewport": 320,
    "loading": true,
    "version": "1.2.0",
    "since": "1.0.0"
  },
  "docs": "## @interlace/newsletter-form\n\nInstalled to `components/ui/patterns/newsletter-form.tsx`.\n\n```tsx\nimport { /* … */ } from '@/components/ui/patterns/newsletter-form';\n```\n\nProps, a11y contract, live preview and source: https://ds.interlace.tools/c/newsletter-form\n\nRequires the `@interlace/theme` CSS baseline (installed automatically as a registry dependency)."
}
