{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "newsletter-signup",
  "title": "Newsletter Signup",
  "author": "ilinxa",
  "description": "Newsletter signup card — inline email form or CTA-only variant, async status tracking, three tones, full i18n.",
  "dependencies": [],
  "registryDependencies": [
    "button",
    "input"
  ],
  "files": [
    {
      "path": "src/registry/components/marketing/newsletter-signup/newsletter-signup.tsx",
      "content": "\"use client\";\n\nimport { memo, useCallback, useId, useMemo, useState } from \"react\";\nimport type { FormEvent, MouseEvent } from \"react\";\nimport { CtaOnlyPart } from \"./parts/cta-only\";\nimport { InlineFormPart } from \"./parts/inline-form\";\nimport {\n  DEFAULT_LABELS,\n  type NewsletterSignupProps,\n  type NewsletterSignupStatus,\n  type ResolvedPartProps,\n} from \"./types\";\n\n/**\n * NewsletterSignup — brand-tinted CTA card with email signup.\n *\n * Two variants via the `variant` prop:\n * - `inline-form` (default) — email input + Subscribe button.\n * - `cta-only` — full-width button only; for \"click to open signup flow\" patterns.\n *\n * Form state can be controlled (`value` + `onChange`) or uncontrolled\n * (`defaultValue`). Status (`idle | pending | success | error`) can be\n * controlled (`status` prop) or auto-tracked from a Promise-returning\n * `onSubmit`. Fully i18n via the `labels` prop.\n */\nfunction NewsletterSignupImpl(props: NewsletterSignupProps) {\n  const {\n    variant = \"inline-form\",\n    tone = \"primary\",\n    headingAs = \"h3\",\n    value: controlledValue,\n    defaultValue = \"\",\n    onChange,\n    onSubmit,\n    status: controlledStatus,\n    onStatusChange,\n    labels: labelsProp,\n    className,\n    buttonClassName,\n    id: idProp,\n  } = props;\n\n  const generatedId = useId();\n  const baseId = idProp ?? generatedId;\n  const titleId = `${baseId}-title`;\n  const bodyId = `${baseId}-body`;\n\n  const labels = useMemo(\n    () => ({ ...DEFAULT_LABELS, ...labelsProp }),\n    [labelsProp],\n  );\n\n  const [internalValue, setInternalValue] = useState(defaultValue);\n  const isControlled = controlledValue !== undefined;\n  const email = isControlled ? controlledValue : internalValue;\n\n  const handleEmailChange = useCallback(\n    (next: string) => {\n      if (!isControlled) setInternalValue(next);\n      onChange?.(next);\n    },\n    [isControlled, onChange],\n  );\n\n  const [internalStatus, setInternalStatus] = useState<NewsletterSignupStatus>(\"idle\");\n  const status = controlledStatus ?? internalStatus;\n\n  const setStatus = useCallback(\n    (next: NewsletterSignupStatus) => {\n      if (controlledStatus === undefined) setInternalStatus(next);\n      onStatusChange?.(next);\n    },\n    [controlledStatus, onStatusChange],\n  );\n\n  const handleSubmit = useCallback(\n    (event: FormEvent | MouseEvent) => {\n      event.preventDefault();\n      if (status === \"pending\") return;\n      if (!onSubmit) return;\n\n      const result = onSubmit(email);\n\n      if (result instanceof Promise) {\n        setStatus(\"pending\");\n        result\n          .then(() => setStatus(\"success\"))\n          .catch(() => setStatus(\"error\"));\n      }\n    },\n    [email, onSubmit, setStatus, status],\n  );\n\n  const partProps: ResolvedPartProps = {\n    variant,\n    tone,\n    headingAs,\n    email,\n    onEmailChange: handleEmailChange,\n    status,\n    labels,\n    onSubmit: handleSubmit,\n    className,\n    buttonClassName,\n    titleId,\n    bodyId,\n  };\n\n  if (variant === \"cta-only\") return <CtaOnlyPart {...partProps} />;\n  return <InlineFormPart {...partProps} />;\n}\n\nexport const NewsletterSignup = memo(NewsletterSignupImpl);\nNewsletterSignup.displayName = \"NewsletterSignup\";\n\nexport default NewsletterSignup;\n",
      "type": "registry:component",
      "target": "components/newsletter-signup/newsletter-signup.tsx"
    },
    {
      "path": "src/registry/components/marketing/newsletter-signup/index.ts",
      "content": "export { NewsletterSignup, default } from \"./newsletter-signup\";\nexport type {\n  NewsletterSignupHeadingLevel,\n  NewsletterSignupLabels,\n  NewsletterSignupProps,\n  NewsletterSignupStatus,\n  NewsletterSignupTone,\n  NewsletterSignupVariant,\n} from \"./types\";\n",
      "type": "registry:component",
      "target": "components/newsletter-signup/index.ts"
    },
    {
      "path": "src/registry/components/marketing/newsletter-signup/types.ts",
      "content": "import type { FormEvent, MouseEvent } from \"react\";\n\nexport type NewsletterSignupVariant = \"inline-form\" | \"cta-only\";\n\nexport type NewsletterSignupTone = \"primary\" | \"accent\" | \"muted\";\n\nexport type NewsletterSignupStatus = \"idle\" | \"pending\" | \"success\" | \"error\";\n\nexport type NewsletterSignupHeadingLevel = \"h2\" | \"h3\" | \"h4\";\n\nexport interface NewsletterSignupLabels {\n  /** Headline. Default: 'Join our newsletter'. */\n  title?: string;\n  /** Body / description. Default: 'Latest updates, straight to your inbox.'. */\n  body?: string;\n  /**\n   * Email input placeholder. Default: 'you@example.com' — a format-hint\n   * showing the expected shape (per Nielsen \"recognition over recall\"). Pass\n   * 'Email address' if you prefer a generic prompt over an example.\n   */\n  placeholder?: string;\n  /** Email input aria-label. Default: 'Email address'. */\n  emailLabel?: string;\n  /** Button text. Default: 'Subscribe'. */\n  button?: string;\n  /** Success message after `onSubmit` resolves. Default: \"Thanks! You're subscribed.\". */\n  successMessage?: string;\n  /** Error message after `onSubmit` rejects. Default: 'Something went wrong. Please try again.'. */\n  errorMessage?: string;\n}\n\nexport const DEFAULT_LABELS: Required<NewsletterSignupLabels> = {\n  title: \"Join our newsletter\",\n  body: \"Latest updates, straight to your inbox.\",\n  placeholder: \"you@example.com\",\n  emailLabel: \"Email address\",\n  button: \"Subscribe\",\n  successMessage: \"Thanks! You're subscribed.\",\n  errorMessage: \"Something went wrong. Please try again.\",\n};\n\nexport interface NewsletterSignupProps {\n  /** Visual variant. Default: 'inline-form'. */\n  variant?: NewsletterSignupVariant;\n\n  /** Color tint. Default: 'primary'. */\n  tone?: NewsletterSignupTone;\n\n  /** Heading semantic level. Default: 'h3'. */\n  headingAs?: NewsletterSignupHeadingLevel;\n\n  /** Controlled email value. */\n  value?: string;\n  /** Uncontrolled initial email value. */\n  defaultValue?: string;\n  /** Email change callback. */\n  onChange?: (value: string) => void;\n\n  /** Submit handler. Return Promise to auto-track pending → success/error. */\n  onSubmit?: (email: string) => void | Promise<void>;\n\n  /** Controlled status. If omitted, derives from `onSubmit`'s return. */\n  status?: NewsletterSignupStatus;\n  /** Status change callback. */\n  onStatusChange?: (status: NewsletterSignupStatus) => void;\n\n  /** Localized labels. Defaults are English. */\n  labels?: NewsletterSignupLabels;\n\n  /** Override classes for the root container. */\n  className?: string;\n  /** Override classes for the action button. */\n  buttonClassName?: string;\n\n  /** Override the heading id. Default: useId(). */\n  id?: string;\n}\n\n/** Internal shape passed from the root to each variant part. */\nexport interface ResolvedPartProps {\n  variant: NewsletterSignupVariant;\n  tone: NewsletterSignupTone;\n  headingAs: NewsletterSignupHeadingLevel;\n  email: string;\n  onEmailChange: (value: string) => void;\n  status: NewsletterSignupStatus;\n  labels: Required<NewsletterSignupLabels>;\n  onSubmit: (event: FormEvent | MouseEvent) => void;\n  className: string | undefined;\n  buttonClassName: string | undefined;\n  titleId: string;\n  bodyId: string;\n}\n",
      "type": "registry:component",
      "target": "components/newsletter-signup/types.ts"
    },
    {
      "path": "src/registry/components/marketing/newsletter-signup/parts/inline-form.tsx",
      "content": "import { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { cn } from \"@/lib/utils\";\nimport type { ResolvedPartProps } from \"../types\";\nimport { resolveTone } from \"./tone\";\nimport { StatusMessage } from \"./status-message\";\n\n/**\n * Inline-form variant — email input + Subscribe button in a row.\n * The default for sidebar / footer CTAs that capture email inline.\n */\nexport function InlineFormPart({\n  tone,\n  headingAs,\n  email,\n  onEmailChange,\n  status,\n  labels,\n  onSubmit,\n  className,\n  buttonClassName,\n  titleId,\n  bodyId,\n}: ResolvedPartProps) {\n  const HeadingTag = headingAs;\n  const toneClasses = resolveTone(tone);\n\n  return (\n    <div\n      role=\"region\"\n      aria-labelledby={titleId}\n      className={cn(\n        \"rounded-2xl border p-6\",\n        toneClasses.frame,\n        className,\n      )}\n    >\n      <HeadingTag\n        id={titleId}\n        className={cn(\"mb-2 font-serif text-lg font-bold\", toneClasses.heading)}\n      >\n        {labels.title}\n      </HeadingTag>\n      <p id={bodyId} className=\"mb-4 text-sm text-muted-foreground\">\n        {labels.body}\n      </p>\n\n      <form onSubmit={onSubmit} className=\"flex gap-2\">\n        <Input\n          type=\"email\"\n          required\n          value={email}\n          onChange={(event) => onEmailChange(event.target.value)}\n          placeholder={labels.placeholder}\n          aria-label={labels.emailLabel}\n          aria-describedby={bodyId}\n          aria-invalid={status === \"error\"}\n          disabled={status === \"pending\"}\n          className=\"flex-1\"\n        />\n        <Button\n          type=\"submit\"\n          disabled={status === \"pending\"}\n          aria-busy={status === \"pending\"}\n          className={buttonClassName}\n        >\n          {labels.button}\n        </Button>\n      </form>\n\n      <StatusMessage status={status} labels={labels} />\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/newsletter-signup/parts/inline-form.tsx"
    },
    {
      "path": "src/registry/components/marketing/newsletter-signup/parts/cta-only.tsx",
      "content": "import { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport type { ResolvedPartProps } from \"../types\";\nimport { resolveTone } from \"./tone\";\nimport { StatusMessage } from \"./status-message\";\n\n/**\n * CTA-only variant — full-width Subscribe button without an email input.\n * For \"click to open the signup flow elsewhere\" patterns; consumer's\n * `onSubmit` typically opens a modal or navigates.\n *\n * onSubmit is called with an empty string ('') since no email is captured here.\n */\nexport function CtaOnlyPart({\n  tone,\n  headingAs,\n  status,\n  labels,\n  onSubmit,\n  className,\n  buttonClassName,\n  titleId,\n  bodyId,\n}: ResolvedPartProps) {\n  const HeadingTag = headingAs;\n  const toneClasses = resolveTone(tone);\n\n  return (\n    <div\n      role=\"region\"\n      aria-labelledby={titleId}\n      className={cn(\n        \"rounded-2xl border p-6\",\n        toneClasses.frame,\n        className,\n      )}\n    >\n      <HeadingTag\n        id={titleId}\n        className={cn(\"mb-2 font-serif text-lg font-bold\", toneClasses.heading)}\n      >\n        {labels.title}\n      </HeadingTag>\n      <p id={bodyId} className=\"mb-4 text-sm text-muted-foreground\">\n        {labels.body}\n      </p>\n\n      <Button\n        type=\"button\"\n        onClick={onSubmit}\n        disabled={status === \"pending\"}\n        aria-busy={status === \"pending\"}\n        className={cn(\"w-full\", buttonClassName)}\n      >\n        {labels.button}\n      </Button>\n\n      <StatusMessage status={status} labels={labels} />\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/newsletter-signup/parts/cta-only.tsx"
    },
    {
      "path": "src/registry/components/marketing/newsletter-signup/parts/status-message.tsx",
      "content": "import type { NewsletterSignupStatus, NewsletterSignupLabels } from \"../types\";\n\n/**\n * Status message renderer. Lives below the action area in both variants.\n *\n * - `success` renders in muted text inside an aria-live=\"polite\" region.\n * - `error` renders in destructive text inside a role=\"alert\" region.\n * - `idle` and `pending` render an empty live-region (so the assistive\n *   technology has a stable announcement target when status changes).\n */\nexport function StatusMessage({\n  status,\n  labels,\n}: {\n  status: NewsletterSignupStatus;\n  labels: Required<NewsletterSignupLabels>;\n}) {\n  if (status === \"error\") {\n    return (\n      <div role=\"alert\" className=\"mt-3 text-sm text-destructive\">\n        {labels.errorMessage}\n      </div>\n    );\n  }\n\n  return (\n    <div aria-live=\"polite\" className=\"mt-3 min-h-[1.25rem] text-sm text-muted-foreground\">\n      {status === \"success\" ? labels.successMessage : null}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/newsletter-signup/parts/status-message.tsx"
    },
    {
      "path": "src/registry/components/marketing/newsletter-signup/parts/tone.ts",
      "content": "import type { NewsletterSignupTone } from \"../types\";\n\n/**\n * Tone → Tailwind class fragments. Each tone gets a frame (background +\n * border) + a heading color modifier. The frame is `rounded-2xl border p-6`\n * by default, applied at the part level.\n */\nconst TONE_MAP: Record<NewsletterSignupTone, { frame: string; heading: string }> = {\n  primary: {\n    frame: \"border-primary/20 bg-primary/5\",\n    heading: \"text-foreground\",\n  },\n  accent: {\n    frame: \"border-accent/40 bg-accent/30\",\n    heading: \"text-accent-foreground\",\n  },\n  muted: {\n    frame: \"border-border/50 bg-muted/40\",\n    heading: \"text-foreground\",\n  },\n};\n\nexport const resolveTone = (tone: NewsletterSignupTone) => TONE_MAP[tone];\n",
      "type": "registry:component",
      "target": "components/newsletter-signup/parts/tone.ts"
    }
  ],
  "categories": [
    "marketing"
  ],
  "type": "registry:block"
}