{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "call-to-action-signup",
  "title": "Call To Action Signup",
  "description": "Email-capture Payload call-to-action block: heading, copy, and a form that posts to a same-origin endpoint.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "button"
  ],
  "files": [
    {
      "path": "payload-components/source/blocks/shared/callToActionFields.ts",
      "content": "import type { Field } from 'payload'\n\n/**\n * Shared field core for the Call To Action component family.\n *\n * Every call-to-action variant (call-to-action-centered, call-to-action-boxed,\n * call-to-action-signup, …) spreads these heading fields first and then appends\n * its own variant-specific shape — the CTA link group for the centered and boxed\n * layouts, or the email-capture fields for the signup layout. Editing the shared\n * title/description here updates every installed call-to-action block at once, so\n * the family never drifts field-by-field across a repo.\n *\n * Installed once per repo at `src/blocks/shared/callToActionFields.ts`; re-running\n * `payload-components add call-to-action-*` never overwrites a copy you have already edited.\n */\nexport const callToActionFields: Field[] = [\n  {\n    name: 'title',\n    type: 'text',\n    required: true,\n  },\n  {\n    name: 'description',\n    type: 'textarea',\n  },\n]\n",
      "type": "registry:file",
      "target": "~/src/blocks/shared/callToActionFields.ts"
    },
    {
      "path": "payload-components/source/blocks/shared/safeUrls.ts",
      "content": "const approvedEmbedHosts = new Set([\n  'airtable.com',\n  'docs.google.com',\n  'form.typeform.com',\n  'lookerstudio.google.com',\n  'maps.google.com',\n  'player.vimeo.com',\n  'vimeo.com',\n  'www.airtable.com',\n  'www.google.com',\n  'www.youtube-nocookie.com',\n  'www.youtube.com',\n  'youtube-nocookie.com',\n  'youtube.com',\n])\n\nconst approvedEmbedHostSuffixes = [\n  '.airtable.com',\n  '.google.com',\n  '.typeform.com',\n  '.vimeo.com',\n  '.youtube-nocookie.com',\n  '.youtube.com',\n]\n\nconst embedUrlError = 'Use an approved HTTPS embed URL.'\nconst formActionError = 'Use a same-origin path, such as /api/newsletter.'\nconst sameOriginBase = 'https://payload-components.local'\n\nconst isApprovedEmbedHost = (hostname: string) => {\n  const normalizedHost = hostname.toLowerCase()\n\n  return (\n    approvedEmbedHosts.has(normalizedHost) ||\n    approvedEmbedHostSuffixes.some((suffix) => normalizedHost.endsWith(suffix))\n  )\n}\n\nexport const getSafeEmbedUrl = (value: unknown) => {\n  if (typeof value !== 'string') return undefined\n\n  const trimmed = value.trim()\n\n  if (!trimmed) return undefined\n\n  try {\n    const parsed = new URL(trimmed)\n\n    if (parsed.protocol !== 'https:' || parsed.username || parsed.password) {\n      return undefined\n    }\n\n    if (!isApprovedEmbedHost(parsed.hostname)) {\n      return undefined\n    }\n\n    return parsed.toString()\n  } catch {\n    return undefined\n  }\n}\n\nexport const validateEmbedUrl = (value: unknown) => getSafeEmbedUrl(value) ? true : embedUrlError\n\nexport const getSafeFormAction = (value: unknown) => {\n  if (typeof value !== 'string') return undefined\n\n  const trimmed = value.trim()\n\n  if (!trimmed || !trimmed.startsWith('/') || trimmed.startsWith('//') || trimmed.includes('\\\\')) {\n    return undefined\n  }\n\n  try {\n    const parsed = new URL(trimmed, sameOriginBase)\n\n    if (parsed.origin !== sameOriginBase) {\n      return undefined\n    }\n\n    return `${parsed.pathname}${parsed.search}${parsed.hash}`\n  } catch {\n    return undefined\n  }\n}\n\nexport const validateSameOriginFormAction = (value: unknown) => {\n  if (value === null || value === undefined || (typeof value === 'string' && value.trim() === '')) {\n    return true\n  }\n\n  return getSafeFormAction(value) ? true : formActionError\n}\n",
      "type": "registry:file",
      "target": "~/src/blocks/shared/safeUrls.ts"
    },
    {
      "path": "payload-components/source/blocks/CallToActionSignup/config.ts",
      "content": "import type { Block } from 'payload'\n\nimport { callToActionFields } from '@/blocks/shared/callToActionFields'\nimport { validateSameOriginFormAction } from '@/blocks/shared/safeUrls'\n\nexport const CallToActionSignup: Block = {\n  slug: 'callToActionSignup',\n  // Existing apps must migrate stored data before adopting this identifier:\n  // https://www.payload-components.xyz/docs/registry#installed-source-and-migrations\n  dbName: 'pc_cal_to_act_sig',\n  interfaceName: 'CallToActionSignupBlock',\n  fields: [\n    // Shared call-to-action core (title, description). Variant-specific fields\n    // follow; edit the shared shape in @/blocks/shared/callToActionFields.\n    ...callToActionFields,\n    {\n      name: 'emailPlaceholder',\n      type: 'text',\n    },\n    {\n      name: 'submitLabel',\n      type: 'text',\n    },\n    {\n      name: 'action',\n      type: 'text',\n      validate: validateSameOriginFormAction,\n      admin: {\n        description: 'Same-origin path where the email form posts, such as /api/newsletter.',\n      },\n    },\n  ],\n  labels: {\n    plural: 'Call To Action Signup Blocks',\n    singular: 'Call To Action Signup',\n  },\n}\n",
      "type": "registry:file",
      "target": "~/src/blocks/CallToActionSignup/config.ts"
    },
    {
      "path": "payload-components/source/blocks/CallToActionSignup/Component.tsx",
      "content": "import React from 'react'\n\nimport type { CallToActionSignupBlock as CallToActionSignupBlockData } from '@/payload-types'\n\nimport { Mail, SendHorizonal } from 'lucide-react'\n\nimport { getSafeFormAction } from '@/blocks/shared/safeUrls'\nimport { Button } from '@/components/ui/button'\nimport { cn } from '@/utilities/ui'\n\ntype Props = CallToActionSignupBlockData & {\n  id?: string\n  className?: string\n  disableInnerContainer?: boolean\n}\n\nexport const CallToActionSignupBlock: React.FC<Props> = ({\n  action,\n  className,\n  description,\n  disableInnerContainer,\n  emailPlaceholder,\n  id,\n  submitLabel,\n  title,\n}) => {\n  const formAction = getSafeFormAction(action) ?? '#'\n\n  return (\n    <section className={cn('container', className)} id={id ? `block-${id}` : undefined}>\n      <div className=\"overflow-hidden rounded-frame border border-border/70 bg-card/35 px-6 py-10 sm:px-8 lg:px-12 lg:py-14\">\n        <div\n          className={cn('flex flex-col items-center gap-4 text-center', {\n            'mx-auto max-w-2xl': !disableInnerContainer,\n          })}\n        >\n          <h2 className=\"text-4xl font-medium tracking-display text-balance sm:text-5xl\">{title}</h2>\n\n          {description ? (\n            <p className=\"text-base leading-7 text-muted-foreground sm:text-lg\">{description}</p>\n          ) : null}\n\n          <form action={formAction} className=\"mt-2 w-full max-w-sm\" method=\"post\">\n            <div className=\"relative grid grid-cols-[1fr_auto] items-center rounded-frame border border-border/70 bg-background/80 pe-2\">\n              <Mail className=\"pointer-events-none absolute inset-y-0 left-4 my-auto size-5 text-muted-foreground\" />\n\n              <input\n                aria-label={emailPlaceholder || 'Email address'}\n                autoComplete=\"email\"\n                className=\"h-12 w-full bg-transparent ps-11 text-sm focus:outline-none\"\n                name=\"email\"\n                placeholder={emailPlaceholder || 'Your email address'}\n                required\n                type=\"email\"\n              />\n\n              <Button aria-label=\"Subscribe\" type=\"submit\">\n                <span className=\"hidden sm:block\">{submitLabel || 'Get Started'}</span>\n                <SendHorizonal className=\"size-4 sm:hidden\" strokeWidth={2} />\n              </Button>\n            </div>\n          </form>\n        </div>\n      </div>\n    </section>\n  )\n}\n",
      "type": "registry:file",
      "target": "~/src/blocks/CallToActionSignup/Component.tsx"
    }
  ],
  "meta": {
    "payloadComponent": {
      "installCommand": "payload-components add call-to-action-signup",
      "postInstall": [
        "generate:types",
        "generate:importmap"
      ],
      "requiresPayloadComponentWrapper": true,
      "supportedTargets": [
        "payload-website-starter",
        "payload-blocks-app"
      ]
    }
  },
  "docs": "Payload Components installs this as a Payload CMS block for the Payload website starter shape.\n\nRecommended install:\n\n```bash\npnpm payload-components add call-to-action-signup\n```\n\nDirect shadcn install URL:\n\n```bash\npnpm dlx shadcn@latest add https://www.payload-components.xyz/r/call-to-action-signup.json\n```\n\nDirect shadcn installs only copy the block source files and shadcn UI dependencies. Use `payload-components add call-to-action-signup` when you also want Payload layout registration, `RenderBlocks` wiring, `generate:types`, and `generate:importmap` handled for you.",
  "type": "registry:block"
}