{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "footer-centered",
  "title": "Footer Centered",
  "description": "A centred brand, tagline, and navigation above a rule carrying copyright and policy links.",
  "registryDependencies": [],
  "files": [
    {
      "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/shared/footerUrls.ts",
      "content": "import { getSafeFormAction } from './safeUrls'\n\nconst footerHrefError = 'Enter an HTTPS URL, a mailto:/tel: link, or a same-origin path such as /pricing.'\n\n/**\n * Resolve one footer link `href` to something safe to put on an anchor.\n *\n * Footers are almost entirely a link surface, and React does not sanitize `href`\n * — a stored `javascript:` or `data:` value would execute on click. Everything is\n * rejected here except the four shapes a real footer needs: a same-origin path,\n * an HTTPS URL, `mailto:`, and `tel:`. Both the admin `validate` and the render\n * path call through this, so a row written straight through the Local API (which\n * skips field validation) still cannot reach the DOM unchecked.\n */\nexport const getSafeFooterHref = (value: unknown) => {\n  if (typeof value !== 'string') return undefined\n\n  const trimmed = value.trim()\n\n  if (!trimmed || /[\\r\\n\\t]/.test(trimmed)) return undefined\n\n  const sameOriginPath = getSafeFormAction(trimmed)\n  if (sameOriginPath) return sameOriginPath\n\n  try {\n    const parsed = new URL(trimmed)\n\n    if (parsed.username || parsed.password) return undefined\n\n    if (parsed.protocol === 'https:') return parsed.toString()\n\n    if (parsed.protocol === 'mailto:' || parsed.protocol === 'tel:') {\n      return parsed.toString()\n    }\n\n    return undefined\n  } catch {\n    return undefined\n  }\n}\n\nexport const validateFooterHref = (value: unknown) =>\n  getSafeFooterHref(value) ? true : footerHrefError\n",
      "type": "registry:file",
      "target": "~/src/blocks/shared/footerUrls.ts"
    },
    {
      "path": "payload-components/source/blocks/shared/footerFields.ts",
      "content": "import type { Field } from 'payload'\n\nimport { validateFooterHref } from '@/blocks/shared/footerUrls'\n\n/**\n * Shared field core for the Footer component family.\n *\n * Every footer variant (footer-columns, footer-simple, footer-centered, …)\n * spreads `footerFields` first for the shared brand and copyright line, then\n * appends its own variant-specific shape — the grouped link columns, the flat\n * inline nav, or the centred stack with social links. Editing the shared brand\n * shape here updates every installed footer block at once, so the family never\n * drifts field-by-field across a repo.\n *\n * `brandLabel` is required even when a `logo` upload is set: it is the accessible\n * name for the home link, so an icon-only mark still announces itself to a screen\n * reader instead of reading as a bare image. When no logo is uploaded it also\n * renders as the visible text wordmark.\n *\n * `footerLinkFields` is the one-link shape reused by every variant. Its `href` is\n * validated with `validateFooterHref` in the admin and re-checked at render with\n * `getSafeFooterHref`, so an unexpected scheme cannot reach the DOM even if a row\n * was written straight through the Local API — see `@/blocks/shared/footerUrls`.\n *\n * Installed once per repo at `src/blocks/shared/footerFields.ts`; re-running\n * `payload-components add footer-*` never overwrites a copy you have already edited.\n */\nexport const footerFields: Field[] = [\n  {\n    name: 'logo',\n    type: 'upload',\n    relationTo: 'media',\n    admin: {\n      description: 'Brand mark shown in the footer. Falls back to Brand label when empty.',\n    },\n  },\n  {\n    name: 'brandLabel',\n    type: 'text',\n    required: true,\n    admin: {\n      description:\n        'Accessible name for the home link, and the visible wordmark when no logo is uploaded.',\n    },\n  },\n  {\n    name: 'copyright',\n    type: 'text',\n    required: true,\n  },\n]\n\nexport const footerLinkFields: Field[] = [\n  {\n    name: 'label',\n    type: 'text',\n    required: true,\n  },\n  {\n    name: 'href',\n    type: 'text',\n    required: true,\n    validate: validateFooterHref,\n  },\n]\n",
      "type": "registry:file",
      "target": "~/src/blocks/shared/footerFields.ts"
    },
    {
      "path": "payload-components/source/blocks/FooterCentered/config.ts",
      "content": "import type { Block } from 'payload'\n\nimport { footerFields, footerLinkFields } from '@/blocks/shared/footerFields'\n\nexport const FooterCentered: Block = {\n  slug: 'footerCentered',\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_foo_cen',\n  interfaceName: 'FooterCenteredBlock',\n  fields: [\n    // Shared footer core (logo, brandLabel, copyright). Variant-specific fields\n    // follow; edit the shared shape in @/blocks/shared/footerFields.\n    ...footerFields,\n    {\n      name: 'tagline',\n      type: 'textarea',\n    },\n    {\n      name: 'links',\n      type: 'array',\n      required: true,\n      minRows: 1,\n      maxRows: 8,\n      admin: {\n        description: 'Primary navigation, centred under the brand.',\n        initCollapsed: true,\n      },\n      // Shared link shape — see @/blocks/shared/footerFields.\n      fields: footerLinkFields,\n    },\n    {\n      name: 'legalLinks',\n      type: 'array',\n      minRows: 1,\n      maxRows: 4,\n      admin: {\n        description: 'Policy links shown beside the copyright line — privacy, terms, cookies.',\n        initCollapsed: true,\n      },\n      // Shared link shape — see @/blocks/shared/footerFields.\n      fields: footerLinkFields,\n    },\n  ],\n  labels: {\n    plural: 'Footer Centered Blocks',\n    singular: 'Footer Centered',\n  },\n}\n",
      "type": "registry:file",
      "target": "~/src/blocks/FooterCentered/config.ts"
    },
    {
      "path": "payload-components/source/blocks/FooterCentered/Component.tsx",
      "content": "import React from 'react'\n\nimport type { FooterCenteredBlock as FooterCenteredBlockData } from '@/payload-types'\n\nimport { getSafeFooterHref } from '@/blocks/shared/footerUrls'\nimport { Media } from '@/components/Media'\nimport { cn } from '@/utilities/ui'\n\n// Layout adapted from tailark/blocks (MIT) — re-implemented as a Payload block.\n\ntype Props = FooterCenteredBlockData & {\n  id?: string\n  className?: string\n  disableInnerContainer?: boolean\n}\n\nexport const FooterCenteredBlock: React.FC<Props> = ({\n  brandLabel,\n  className,\n  copyright,\n  disableInnerContainer,\n  id,\n  legalLinks,\n  links,\n  logo,\n  tagline,\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 gap-10', {\n            'mx-auto max-w-3xl': !disableInnerContainer,\n          })}\n        >\n          <div className=\"flex flex-col items-center gap-4 text-center\">\n            {logo ? (\n              <Media resource={logo} imgClassName=\"h-8 w-auto object-contain\" />\n            ) : (\n              <span className=\"text-lg font-medium tracking-title text-foreground\">{brandLabel}</span>\n            )}\n\n            {tagline ? (\n              <p className=\"max-w-md text-sm leading-6 text-muted-foreground\">{tagline}</p>\n            ) : null}\n          </div>\n\n          <nav aria-label={brandLabel}>\n            <ul className=\"flex flex-wrap justify-center gap-x-6 gap-y-3\" role=\"list\">\n              {links?.map((link, index) => {\n                const href = getSafeFooterHref(link.href)\n\n                if (!href) return null\n\n                return (\n                  <li key={link.id ?? `${link.label}-${index}`}>\n                    <a\n                      className=\"text-sm leading-6 text-muted-foreground transition-colors hover:text-foreground\"\n                      href={href}\n                    >\n                      {link.label}\n                    </a>\n                  </li>\n                )\n              })}\n            </ul>\n          </nav>\n\n          <div className=\"flex flex-col-reverse items-center gap-4 border-t border-border/70 pt-8 sm:flex-row sm:justify-between\">\n            <p className=\"text-sm text-muted-foreground\">{copyright}</p>\n\n            {legalLinks && legalLinks.length > 0 ? (\n              <ul className=\"flex flex-wrap justify-center gap-x-6 gap-y-3\" role=\"list\">\n                {legalLinks.map((link, index) => {\n                  const href = getSafeFooterHref(link.href)\n\n                  if (!href) return null\n\n                  return (\n                    <li key={link.id ?? `${link.label}-${index}`}>\n                      <a\n                        className=\"text-sm leading-6 text-muted-foreground transition-colors hover:text-foreground\"\n                        href={href}\n                      >\n                        {link.label}\n                      </a>\n                    </li>\n                  )\n                })}\n              </ul>\n            ) : null}\n          </div>\n        </div>\n      </div>\n    </section>\n  )\n}\n",
      "type": "registry:file",
      "target": "~/src/blocks/FooterCentered/Component.tsx"
    }
  ],
  "meta": {
    "payloadComponent": {
      "installCommand": "payload-components add footer-centered",
      "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 footer-centered\n```\n\nDirect shadcn install URL:\n\n```bash\npnpm dlx shadcn@latest add https://www.payload-components.xyz/r/footer-centered.json\n```\n\nDirect shadcn installs only copy the block source files and shadcn UI dependencies. Use `payload-components add footer-centered` when you also want Payload layout registration, `RenderBlocks` wiring, `generate:types`, and `generate:importmap` handled for you.",
  "type": "registry:block"
}