{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "embed-basic",
  "title": "Embed Basic",
  "description": "Responsive, accessible sandboxed iframe block for approved HTTPS embeds.",
  "registryDependencies": [],
  "files": [
    {
      "path": "payload-components/source/blocks/EmbedBasic/config.ts",
      "content": "import type { Block } from 'payload'\n\nimport { validateEmbedUrl } from '@/blocks/shared/safeUrls'\n\nexport const EmbedBasic: Block = {\n  slug: 'embedBasic',\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_emb_bas',\n  interfaceName: 'EmbedBasicBlock',\n  fields: [\n    {\n      name: 'url',\n      type: 'text',\n      required: true,\n      validate: validateEmbedUrl,\n      admin: {\n        description:\n          'Approved HTTPS embed URL (e.g. https://www.youtube.com/embed/VIDEO_ID).',\n      },\n    },\n    {\n      name: 'title',\n      type: 'text',\n      required: true,\n      admin: {\n        description: 'Accessible title announced to screen readers for the embedded frame.',\n      },\n    },\n    {\n      name: 'aspectRatio',\n      type: 'select',\n      required: true,\n      defaultValue: '16:9',\n      options: [\n        { label: '16:9 (widescreen video)', value: '16:9' },\n        { label: '4:3 (standard video)', value: '4:3' },\n        { label: '1:1 (square)', value: '1:1' },\n        { label: '21:9 (ultrawide)', value: '21:9' },\n      ],\n    },\n    {\n      name: 'caption',\n      type: 'text',\n    },\n    {\n      name: 'allowFullscreen',\n      type: 'checkbox',\n      defaultValue: true,\n    },\n  ],\n  labels: {\n    plural: 'Embed Basic Blocks',\n    singular: 'Embed Basic',\n  },\n}\n",
      "type": "registry:file",
      "target": "~/src/blocks/EmbedBasic/config.ts"
    },
    {
      "path": "payload-components/source/blocks/EmbedBasic/Component.tsx",
      "content": "import React from 'react'\n\nimport type { EmbedBasicBlock as EmbedBasicBlockData } from '@/payload-types'\n\nimport { getSafeEmbedUrl } from '@/blocks/shared/safeUrls'\nimport { cn } from '@/utilities/ui'\n\ntype Props = EmbedBasicBlockData & {\n  id?: string\n  className?: string\n}\n\nconst aspectClassByRatio: Record<NonNullable<EmbedBasicBlockData['aspectRatio']>, string> = {\n  '16:9': 'aspect-video',\n  '4:3': 'aspect-[4/3]',\n  '1:1': 'aspect-square',\n  '21:9': 'aspect-[21/9]',\n}\n\nexport const EmbedBasicBlock: React.FC<Props> = ({\n  allowFullscreen,\n  aspectRatio,\n  caption,\n  className,\n  id,\n  title,\n  url,\n}) => {\n  const aspectClass = aspectClassByRatio[aspectRatio ?? '16:9'] ?? aspectClassByRatio['16:9']\n  const safeUrl = getSafeEmbedUrl(url)\n\n  return (\n    <section className={cn('container', className)} id={id ? `block-${id}` : undefined}>\n      <figure className=\"overflow-hidden rounded-frame border border-border/70 bg-card/35\">\n        <div className={cn('relative w-full bg-muted', aspectClass)}>\n          {safeUrl ? (\n            <iframe\n              allow=\"encrypted-media; picture-in-picture\"\n              allowFullScreen={allowFullscreen ?? true}\n              className=\"absolute inset-0 h-full w-full border-0\"\n              loading=\"lazy\"\n              referrerPolicy=\"strict-origin-when-cross-origin\"\n              sandbox=\"allow-forms allow-popups allow-presentation allow-scripts\"\n              src={safeUrl}\n              title={title}\n            />\n          ) : null}\n        </div>\n\n        {caption ? (\n          <figcaption className=\"px-6 py-4 text-sm text-muted-foreground\">{caption}</figcaption>\n        ) : null}\n      </figure>\n    </section>\n  )\n}\n",
      "type": "registry:file",
      "target": "~/src/blocks/EmbedBasic/Component.tsx"
    },
    {
      "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"
    }
  ],
  "meta": {
    "payloadComponent": {
      "installCommand": "payload-components add embed-basic",
      "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 embed-basic\n```\n\nDirect shadcn install URL:\n\n```bash\npnpm dlx shadcn@latest add https://www.payload-components.xyz/r/embed-basic.json\n```\n\nDirect shadcn installs only copy the block source files and shadcn UI dependencies. Use `payload-components add embed-basic` when you also want Payload layout registration, `RenderBlocks` wiring, `generate:types`, and `generate:importmap` handled for you.",
  "type": "registry:block"
}