{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "newsletter-form",
  "title": "Newsletter Form",
  "description": "Email signup with consent, validation, submission, success, and duplicate-email states.",
  "dependencies": [
    "class-variance-authority",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "checkbox",
    "input",
    "label",
    "https://uaiblocks.dev/r/uai-theme.json",
    "https://uaiblocks.dev/r/uai-utils.json"
  ],
  "files": [
    {
      "path": "src/registry/uai/components/newsletter-form.tsx",
      "content": "\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { Check } from \"lucide-react\";\nimport {\n  type ComponentProps,\n  createContext,\n  type FormEvent,\n  useContext,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/uai-utils\";\n\nexport const NEWSLETTER_FORM_VARIANTS = [\"inline\", \"stacked\", \"card\"] as const;\nexport type NewsletterFormVariant = (typeof NEWSLETTER_FORM_VARIANTS)[number];\nexport type NewsletterFormStatus =\n  | \"idle\"\n  | \"invalid\"\n  | \"consent\"\n  | \"submitting\"\n  | \"success\"\n  | \"duplicate\"\n  | \"error\";\nexport type NewsletterFormResult = \"success\" | \"duplicate\" | \"error\";\nexport type NewsletterSubmission = { email: string; consent: boolean };\nexport type NewsletterFormProps = Omit<ComponentProps<\"form\">, \"onSubmit\"> & {\n  variant?: NewsletterFormVariant;\n  /** Resolve \"duplicate\" for existing subscribers; throwing or resolving \"error\" shows a retry message. */\n  onSubscribe: (\n    submission: NewsletterSubmission,\n    // biome-ignore lint/suspicious/noConfusingVoidType: async handlers without a result mean success.\n  ) => Promise<NewsletterFormResult | void> | NewsletterFormResult | void;\n  defaultEmail?: string;\n};\ntype NewsletterContext = {\n  id: string;\n  variant: NewsletterFormVariant;\n  email: string;\n  setEmail: (email: string) => void;\n  consent: boolean;\n  setConsent: (consent: boolean) => void;\n  requireConsent: (required: boolean) => void;\n  status: NewsletterFormStatus;\n  inputRef: React.RefObject<HTMLInputElement | null>;\n  consentRef: React.RefObject<HTMLButtonElement | null>;\n};\nconst Context = createContext<NewsletterContext | null>(null);\nfunction useNewsletter(part: string) {\n  const context = useContext(Context);\n  if (!context) throw new Error(`${part} must be used within NewsletterForm`);\n  return context;\n}\nconst useIsomorphicLayoutEffect = typeof window === \"undefined\" ? useEffect : useLayoutEffect;\nconst EMAIL = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\nconst dangerText = \"text-destructive-ink\";\nconst successText = \"text-success-ink\";\nconst invalidRing = \"shadow-[0_0_0_1px_color-mix(in_oklab,var(--destructive)_70%,transparent)]\";\nconst fieldRing =\n  \"shadow-[0_0_0_1px_var(--border)] focus-within:shadow-[0_0_0_1px_var(--border-strong),0_0_0_4px_color-mix(in_oklab,var(--primary)_14%,transparent)]\";\nconst inputRing =\n  \"shadow-[0_0_0_1px_var(--border)] focus:shadow-[0_0_0_1px_var(--border-strong),0_0_0_4px_color-mix(in_oklab,var(--primary)_14%,transparent)]\";\n\nconst newsletterFormVariants = cva(\"box-border grid min-w-0 text-[13px]/[18px] text-foreground\", {\n  variants: {\n    variant: {\n      inline: \"gap-2\",\n      stacked: \"gap-3\",\n      card: \"gap-3 rounded-[14px] border bg-card p-5\",\n    },\n  },\n});\n\nconst newsletterFormFieldVariants = cva(\"min-w-0 items-center\", {\n  variants: {\n    variant: {\n      inline:\n        \"flex gap-1 rounded-full bg-card py-1 pr-1 pl-3.5 transition-shadow duration-120 ease-out motion-reduce:transition-none\",\n      stacked: \"grid gap-2 rounded-none bg-transparent p-0 shadow-none\",\n      card: \"flex gap-1 rounded-full bg-background py-1 pr-1 pl-3.5 transition-shadow duration-120 ease-out motion-reduce:transition-none\",\n    },\n  },\n});\n\nexport function NewsletterForm({\n  variant = \"inline\",\n  onSubscribe,\n  defaultEmail = \"\",\n  children,\n  className,\n  ...props\n}: NewsletterFormProps) {\n  const id = useId();\n  const inputRef = useRef<HTMLInputElement>(null);\n  const consentRef = useRef<HTMLButtonElement>(null);\n  const [email, setEmailState] = useState(defaultEmail);\n  const [consent, setConsentState] = useState(false);\n  const [consentRequired, requireConsent] = useState(false);\n  const [status, setStatus] = useState<NewsletterFormStatus>(\"idle\");\n  const reset = () => setStatus((current) => (current === \"submitting\" ? current : \"idle\"));\n  const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {\n    event.preventDefault();\n    if (status === \"submitting\") return;\n    const address = email.trim();\n    if (!EMAIL.test(address)) {\n      setStatus(\"invalid\");\n      inputRef.current?.focus();\n      return;\n    }\n    if (consentRequired && !consent) {\n      setStatus(\"consent\");\n      consentRef.current?.focus();\n      return;\n    }\n    setStatus(\"submitting\");\n    try {\n      const result = await onSubscribe({ email: address, consent });\n      setStatus(typeof result === \"string\" ? result : \"success\");\n    } catch {\n      setStatus(\"error\");\n    }\n  };\n  return (\n    <Context.Provider\n      value={{\n        id,\n        variant,\n        email,\n        setEmail: (next) => {\n          setEmailState(next);\n          reset();\n        },\n        consent,\n        setConsent: (next) => {\n          setConsentState(next);\n          reset();\n        },\n        requireConsent,\n        status,\n        inputRef,\n        consentRef,\n      }}\n    >\n      <form\n        noValidate\n        aria-busy={status === \"submitting\"}\n        data-slot=\"newsletter-form\"\n        data-variant={variant}\n        className={cn(newsletterFormVariants({ variant }), className)}\n        {...props}\n        onSubmit={handleSubmit}\n      >\n        {children}\n      </form>\n    </Context.Provider>\n  );\n}\n\nexport function NewsletterFormLabel({ className, ...props }: ComponentProps<\"label\">) {\n  const context = useNewsletter(\"NewsletterFormLabel\");\n  return (\n    <Label\n      data-slot=\"newsletter-form-label\"\n      className={cn(\"text-[13px] leading-[inherit] font-medium select-auto\", className)}\n      {...props}\n      htmlFor={`${context.id}-email`}\n    />\n  );\n}\n\nexport function NewsletterFormField({ className, ...props }: ComponentProps<\"div\">) {\n  const context = useNewsletter(\"NewsletterFormField\");\n  const invalid = context.status === \"invalid\" || context.status === \"duplicate\";\n  return (\n    <div\n      data-slot=\"newsletter-form-field\"\n      className={cn(\n        newsletterFormFieldVariants({ variant: context.variant }),\n        context.variant !== \"stacked\" && (invalid ? invalidRing : fieldRing),\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function NewsletterFormInput({\n  className,\n  onChange,\n  ...props\n}: Omit<ComponentProps<\"input\">, \"value\" | \"defaultValue\" | \"id\" | \"type\">) {\n  const context = useNewsletter(\"NewsletterFormInput\");\n  const stacked = context.variant === \"stacked\";\n  const invalid = context.status === \"invalid\" || context.status === \"duplicate\";\n  return (\n    <Input\n      name=\"email\"\n      autoComplete=\"email\"\n      inputMode=\"email\"\n      data-slot=\"newsletter-form-input\"\n      className={cn(\n        \"box-border w-full min-w-0 flex-1 border-0 bg-transparent text-[13px]/[18px] text-inherit shadow-none outline-none transition-shadow duration-120 ease-out placeholder:text-subtle-foreground placeholder:opacity-100 focus-visible:ring-0 motion-reduce:transition-none md:text-[13px]/[18px] dark:bg-transparent\",\n        stacked ? \"h-9 rounded-[10px] px-3 py-0\" : \"h-7 rounded-none p-0\",\n        stacked && (invalid ? invalidRing : inputRing),\n        className,\n      )}\n      {...props}\n      ref={context.inputRef}\n      id={`${context.id}-email`}\n      type=\"email\"\n      required\n      value={context.email}\n      aria-invalid={invalid || undefined}\n      aria-describedby={`${context.id}-message`}\n      onChange={(event) => {\n        onChange?.(event);\n        if (!event.defaultPrevented) context.setEmail(event.target.value);\n      }}\n    />\n  );\n}\n\nexport function NewsletterFormSubmit({\n  children = \"Subscribe\",\n  pendingLabel = \"Subscribing…\",\n  successLabel = \"Subscribed\",\n  onClick,\n  className,\n  ...props\n}: Omit<ComponentProps<\"button\">, \"type\"> & { pendingLabel?: string; successLabel?: string }) {\n  const context = useNewsletter(\"NewsletterFormSubmit\");\n  const pending = context.status === \"submitting\";\n  const subscribed = context.status === \"success\";\n  const inert = pending || subscribed;\n  const stacked = context.variant === \"stacked\";\n  const label = subscribed ? \"success\" : pending ? \"pending\" : \"idle\";\n  return (\n    <Button\n      data-slot=\"newsletter-form-submit\"\n      variant=\"default\"\n      className={cn(\n        \"flex-none rounded-full border-0 bg-primary px-3.5 py-0 text-[13px] font-medium whitespace-nowrap text-primary-foreground [transition:filter_120ms_ease-out,opacity_120ms_ease-out,scale_140ms_cubic-bezier(0.23,1,0.32,1)] hover:bg-primary focus-visible:ring-0 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:outline-solid has-[>svg]:px-3.5 motion-reduce:transition-none\",\n        stacked ? \"h-8.5\" : \"h-7.5\",\n        subscribed\n          ? \"cursor-default gap-1.5 bg-success/14 text-success-ink hover:bg-success/14\"\n          : pending\n            ? \"cursor-progress opacity-70\"\n            : \"cursor-pointer hover:brightness-108 active:scale-[0.97] motion-reduce:active:scale-100\",\n        className,\n      )}\n      {...props}\n      type=\"submit\"\n      data-status={label}\n      aria-disabled={inert || undefined}\n      onClick={(event) => {\n        if (inert) event.preventDefault();\n        onClick?.(event);\n      }}\n    >\n      {subscribed ? (\n        <Check\n          className=\"size-3.5 animate-in fade-in-0 zoom-in-50 duration-200 ease-out-quint motion-reduce:animate-none\"\n          strokeWidth={2.2}\n          aria-hidden=\"true\"\n        />\n      ) : null}\n      <span className=\"grid [&>*]:[grid-area:1/1]\">\n        <span\n          className={cn(label !== \"idle\" && \"invisible\")}\n          aria-hidden={label !== \"idle\" || undefined}\n        >\n          {children}\n        </span>\n        <span\n          className={cn(label !== \"pending\" && \"invisible\")}\n          aria-hidden={label !== \"pending\" || undefined}\n        >\n          {pendingLabel}\n        </span>\n        <span\n          className={cn(label !== \"success\" && \"invisible\")}\n          aria-hidden={label !== \"success\" || undefined}\n        >\n          {successLabel}\n        </span>\n      </span>\n    </Button>\n  );\n}\n\nexport function NewsletterFormConsent({\n  required = true,\n  children,\n  className,\n  ...props\n}: Omit<ComponentProps<\"label\">, \"htmlFor\"> & { required?: boolean }) {\n  const context = useNewsletter(\"NewsletterFormConsent\");\n  const { requireConsent } = context;\n  useIsomorphicLayoutEffect(() => {\n    requireConsent(required);\n    return () => requireConsent(false);\n  }, [required, requireConsent]);\n  const checkboxId = `${context.id}-consent`;\n  return (\n    <Label\n      data-slot=\"newsletter-form-consent\"\n      className={cn(\n        \"flex cursor-pointer items-start gap-2 text-xs/4 font-normal text-muted-foreground select-auto\",\n        className,\n      )}\n      {...props}\n      htmlFor={checkboxId}\n    >\n      <Checkbox\n        ref={context.consentRef}\n        id={checkboxId}\n        name=\"consent\"\n        required={required}\n        checked={context.consent}\n        aria-invalid={context.status === \"consent\" || undefined}\n        aria-describedby={`${context.id}-message`}\n        onCheckedChange={(checked) => context.setConsent(checked === true)}\n        className=\"m-0 mt-px size-3.5 flex-none cursor-pointer border-subtle-foreground data-[state=checked]:border-primary data-checked:border-primary shadow-none focus-visible:ring-0 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:outline-solid [&_svg]:size-3\"\n      />\n      <span>{children}</span>\n    </Label>\n  );\n}\n\nexport type NewsletterFormMessages = Partial<Record<NewsletterFormStatus, string>>;\nconst COPY: Record<NewsletterFormStatus, string> = {\n  idle: \"\",\n  invalid: \"Enter an email address like name@example.com.\",\n  consent: \"Confirm that you want to receive the newsletter.\",\n  submitting: \"Subscribing…\",\n  success: \"You’re subscribed. Check your inbox to confirm your address.\",\n  duplicate: \"This email is already subscribed. No further action is needed.\",\n  error: \"We couldn’t subscribe you. Please try again.\",\n};\nconst TONE: Partial<Record<NewsletterFormStatus, string>> = {\n  invalid: dangerText,\n  consent: dangerText,\n  error: dangerText,\n  success: successText,\n};\n\nexport function NewsletterFormMessage({\n  messages,\n  children,\n  className,\n  ...props\n}: ComponentProps<\"p\"> & { messages?: NewsletterFormMessages }) {\n  const context = useNewsletter(\"NewsletterFormMessage\");\n  const missing = context.status === \"invalid\" && context.email.trim() === \"\";\n  const copy = missing\n    ? (messages?.invalid ?? \"Enter your email address.\")\n    : (messages?.[context.status] ?? COPY[context.status]);\n  return (\n    <p\n      role=\"status\"\n      data-slot=\"newsletter-form-message\"\n      className={cn(\n        \"m-0 min-h-4 text-xs/4 transition-colors duration-120 ease-out motion-reduce:transition-none\",\n        TONE[context.status] ?? \"text-subtle-foreground\",\n        className,\n      )}\n      {...props}\n      id={`${context.id}-message`}\n    >\n      <span\n        key={context.status}\n        className=\"block animate-in fade-in-0 slide-in-from-top-1 duration-200 ease-out-quint motion-reduce:animate-none\"\n      >\n        {context.status === \"idle\" ? children : copy}\n      </span>\n    </p>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/uai/newsletter-form.tsx"
    }
  ],
  "categories": [
    "marketing",
    "forms"
  ],
  "type": "registry:ui"
}