{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "newsletter-1",
  "title": "Newsletter 1: signup with topics and sample issue",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "An editorial newsletter signup: topic chips, an email field with validation, loading, error and success states, avatar social proof, and a tilted sample issue card beside it.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/button.json",
    "https://ui.ballmac.com/r/input.json",
    "https://ui.ballmac.com/r/media.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/newsletter-1/newsletter-1.tsx",
      "content": "// Ballmac UI: Newsletter 1. https://ui.ballmac.com/blocks/newsletter-1\n\"use client\"\n\nimport * as React from \"react\"\nimport { ArrowRight, Check, Mail } from \"lucide-react\"\n\nimport { Button } from \"@/components/ballmac/button\"\nimport { Input } from \"@/components/ballmac/input\"\nimport { cn } from \"@/lib/utils\"\nimport { Media, type MediaSource } from \"@/components/ballmac/media\"\n\ntype Newsletter1Props = Omit<React.ComponentProps<\"section\">, \"title\" | \"onSubmit\"> & {\n  /** Label above the heading. */\n  eyebrow?: string\n  /** Section heading. */\n  title?: string\n  /** One or two sentences under the heading. */\n  description?: string\n  /** Topics a reader can choose. Pass an empty array to hide them. */\n  topics?: string[]\n  /** Topics selected first. */\n  defaultTopics?: string[]\n  /** Text on the button. */\n  buttonLabel?: string\n  /** Social proof next to the avatars. */\n  proof?: string\n  /** Small print under the form. */\n  note?: string\n  /** Headings shown in the sample issue. */\n  issue?: { name: string; number: string; headlines: string[] }\n  /** A picture of an issue instead of the sample one. An image URL (give it mediaAlt), an object with alt text and a dark-mode file, or your own element. */\n  media?: MediaSource\n  /** Describes `media` when it is a plain URL. */\n  mediaAlt?: string\n  /** Called with the email and the chosen topics. Throw to show an error; resolve to show the thank-you message. */\n  onSubmit?: (email: string, topics: string[]) => void | Promise<void>\n}\n\nconst emailPattern = /^\\S+@\\S+\\.\\S+$/\n\nfunction Newsletter1({\n  eyebrow = \"The Acme Letter\",\n  title = \"One calm email, every other Thursday.\",\n  description = \"Product notes, customer stories and a few things we found worth reading. Written by people, never padded.\",\n  topics = [\"Product updates\", \"Engineering\", \"Design\", \"Customer stories\"],\n  defaultTopics = [\"Product updates\"],\n  buttonLabel = \"Subscribe\",\n  proof = \"Join 12,400 readers\",\n  note = \"No spam, and you can leave with one click.\",\n  issue = {\n    name: \"The Acme Letter\",\n    number: \"Issue 48\",\n    headlines: [\"Why we rebuilt approvals\", \"Five invoices that got paid in a day\", \"Reading list: calm software\"],\n  },\n  onSubmit,\n  media,\n  mediaAlt,\n  className,\n  ...props\n}: Newsletter1Props) {\n  const [email, setEmail] = React.useState(\"\")\n  const [chosen, setChosen] = React.useState<string[]>(defaultTopics)\n  const [state, setState] = React.useState<\"idle\" | \"invalid\" | \"sending\" | \"done\" | \"error\">(\"idle\")\n  const inputId = React.useId()\n  const errorId = `${inputId}-error`\n\n  async function submit(e: React.FormEvent) {\n    e.preventDefault()\n    if (!emailPattern.test(email)) {\n      setState(\"invalid\")\n      return\n    }\n    setState(\"sending\")\n    try {\n      if (onSubmit) await onSubmit(email, chosen)\n      else await new Promise((r) => setTimeout(r, 800))\n      setState(\"done\")\n    } catch {\n      setState(\"error\")\n    }\n  }\n\n  const toggle = (t: string) => setChosen((c) => (c.includes(t) ? c.filter((x) => x !== t) : [...c, t]))\n\n  return (\n    <section data-slot=\"newsletter-1\" className={cn(\"mx-auto max-w-6xl px-4 py-20 sm:px-6 md:py-28\", className)} {...props}>\n      <div className=\"bg-card relative isolate overflow-hidden rounded-[2rem] border\">\n        <div aria-hidden=\"true\" className=\"absolute inset-0 -z-10\">\n          <div className=\"bg-chart-3/20 absolute -top-24 -end-16 size-[24rem] rounded-full blur-[100px]\" />\n          <div className=\"bg-chart-1/15 absolute -bottom-32 -start-10 size-[22rem] rounded-full blur-[100px]\" />\n        </div>\n        <div className=\"grid items-center gap-12 p-8 sm:p-12 lg:grid-cols-[1.1fr_0.9fr] lg:gap-16 lg:p-16\">\n          <div className=\"min-w-0\">\n            <p className=\"text-muted-foreground flex items-center gap-2 text-sm font-medium\"><Mail className=\"size-4\" aria-hidden=\"true\" />{eyebrow}</p>\n            <h2 className=\"mt-4 text-3xl font-semibold tracking-[-0.035em] text-balance sm:text-4xl lg:text-5xl\">{title}</h2>\n            <p className=\"text-muted-foreground mt-4 max-w-lg text-lg text-pretty\">{description}</p>\n\n            {state === \"done\" ? (\n              <div role=\"status\" className=\"mt-8 flex max-w-md items-start gap-3 rounded-2xl border bg-background/70 p-4 backdrop-blur\">\n                <span className=\"bg-chart-2/15 mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-full\"><Check className=\"size-4\" aria-hidden=\"true\" /></span>\n                <p className=\"text-sm\">\n                  <span className=\"block font-semibold\">You’re subscribed.</span>\n                  <span className=\"text-muted-foreground\">Check {email} for a confirmation link. Your first issue arrives soon.</span>\n                </p>\n              </div>\n            ) : (\n              <form onSubmit={submit} noValidate className=\"mt-8 max-w-md\">\n                {topics.length > 0 && (\n                  <fieldset className=\"mb-4\">\n                    <legend className=\"mb-2 text-sm font-medium\">What would you like to read?</legend>\n                    <div className=\"flex flex-wrap gap-1.5\">\n                      {topics.map((t) => (\n                        <button\n                          key={t}\n                          type=\"button\"\n                          aria-pressed={chosen.includes(t)}\n                          onClick={() => toggle(t)}\n                          className={cn(\n                            \"focus-visible:ring-ring/50 inline-flex h-8 items-center gap-1.5 rounded-full border px-3 text-[13px] font-medium outline-none transition-colors focus-visible:ring-[3px]\",\n                            chosen.includes(t) ? \"bg-foreground text-background border-transparent\" : \"bg-background/70 text-muted-foreground hover:text-foreground hover:bg-accent\"\n                          )}\n                        >\n                          {chosen.includes(t) && <Check className=\"size-3.5\" aria-hidden=\"true\" />}\n                          {t}\n                        </button>\n                      ))}\n                    </div>\n                  </fieldset>\n                )}\n                <div className=\"flex flex-col gap-2 sm:flex-row\">\n                  <label htmlFor={inputId} className=\"sr-only\">Email address</label>\n                  <Input\n                    id={inputId}\n                    type=\"email\"\n                    size=\"lg\"\n                    autoComplete=\"email\"\n                    placeholder=\"you@company.com\"\n                    value={email}\n                    onChange={(e) => { setEmail(e.target.value); if (state === \"invalid\") setState(\"idle\") }}\n                    aria-invalid={state === \"invalid\" || undefined}\n                    aria-describedby={state === \"invalid\" || state === \"error\" ? errorId : undefined}\n                    className=\"bg-background/80 sm:flex-1\"\n                  />\n                  <Button type=\"submit\" size=\"lg\" shape=\"pill\" loading={state === \"sending\"}>\n                    {state === \"sending\" ? \"Subscribing…\" : <>{buttonLabel} <ArrowRight  className=\"rtl:rotate-180\"/></>}\n                  </Button>\n                </div>\n                <p id={errorId} role=\"alert\" className=\"text-destructive mt-2 min-h-5 text-sm\">\n                  {state === \"invalid\" && \"Enter a full email address, like name@company.com.\"}\n                  {state === \"error\" && \"We couldn’t subscribe you just now. Please try again.\"}\n                </p>\n                <p className=\"text-muted-foreground text-sm\">{note}</p>\n              </form>\n            )}\n\n            <div className=\"mt-8 flex items-center gap-3\">\n              <div className=\"flex -space-x-1\" aria-hidden=\"true\">\n                {[\"bg-chart-1/30\", \"bg-chart-3/30\", \"bg-chart-5/30\", \"bg-chart-2/30\"].map((c, i) => (\n                  <span key={c} className={cn(\"ring-card flex size-8 items-center justify-center rounded-full text-[11px] font-semibold ring-2\", c)}>{[\"MK\", \"JO\", \"AS\", \"EF\"][i]}</span>\n                ))}\n              </div>\n              <p className=\"text-muted-foreground text-sm\">{proof}</p>\n            </div>\n          </div>\n\n          {/* A sample issue, so people know what they are signing up for. */}\n          <Media media={media} alt={mediaAlt} frame className=\"mx-auto hidden w-full max-w-sm sm:block\" fallback={\n          <div aria-hidden=\"true\" className=\"relative mx-auto hidden w-full max-w-sm sm:block\">\n            <div className=\"bg-background/60 absolute inset-0 translate-x-4 translate-y-4 rotate-3 rounded-2xl border\" />\n            <div className=\"bg-background relative -rotate-2 rounded-2xl border p-6 shadow-[0_30px_70px_-35px_rgb(0_0_0/0.4)] transition-transform duration-500 hover:rotate-0 motion-reduce:transition-none\">\n              <div className=\"flex items-center justify-between border-b pb-4\">\n                <p className=\"font-semibold tracking-tight\">{issue.name}</p>\n                <p className=\"text-muted-foreground font-mono text-xs\">{issue.number}</p>\n              </div>\n              <div className=\"bg-chart-1/20 mt-5 h-24 rounded-xl\" />\n              <ul className=\"mt-5 space-y-4\">\n                {issue.headlines.map((h) => (\n                  <li key={h}>\n                    <p className=\"text-sm leading-snug font-semibold\">{h}</p>\n                    <div className=\"mt-2 space-y-1.5\">\n                      <div className=\"bg-muted h-1.5 w-full rounded-full\" />\n                      <div className=\"bg-muted h-1.5 w-2/3 rounded-full\" />\n                    </div>\n                  </li>\n                ))}\n              </ul>\n            </div>\n          </div>\n          } />\n        </div>\n      </div>\n    </section>\n  )\n}\n\nexport { Newsletter1, type Newsletter1Props }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/newsletter-1/newsletter-1.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "newsletter",
      "signup",
      "email",
      "subscribe",
      "topics",
      "form"
    ],
    "version": "1.0.0",
    "updated": "2026-10-01",
    "ai": {
      "summary": "A newsletter signup section. Pass topics (or [] for none), proof, issue headlines for the preview card, and onSubmit(email, topics); throw to show an error.",
      "whenToUse": [
        "Blogs, changelogs and docs sites",
        "Anywhere readers choose what to receive"
      ],
      "whenNotToUse": [
        "A waitlist over a dramatic background (use cta-2)"
      ],
      "composesWith": [
        "blog-1",
        "changelog-1",
        "footer-2",
        "faq-2"
      ],
      "a11y": [
        {
          "keys": "Enter / Space on a topic",
          "action": "Toggles it (aria-pressed)"
        },
        {
          "keys": "Submit with a bad address",
          "action": "The error is announced as an alert and linked with aria-describedby"
        }
      ],
      "customization": [
        "topics: [] hides the chips",
        "defaultTopics",
        "issue: { name, number, headlines } for the preview card",
        "onSubmit(email, topics)"
      ]
    },
    "examples": [
      "newsletter-1-demo",
      "newsletter-1-simple"
    ],
    "url": "https://ui.ballmac.com/components/newsletter-1"
  },
  "categories": [
    "blocks",
    "newsletter"
  ],
  "type": "registry:block"
}