{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "video-dialog",
  "title": "Video Dialog",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A thumbnail with a play button that opens a video in a focus-trapped dialog, for a video file, an embed URL or a YouTube id from the privacy-friendly domain, mounting the player only while open.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/dialog.json",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/video-dialog.tsx",
      "content": "// Ballmac UI: Video Dialog. https://ui.ballmac.com/components/video-dialog\n\"use client\"\n\nimport * as React from \"react\"\nimport { Play, X } from \"lucide-react\"\n\nimport { Dialog, DialogClose, DialogContent, DialogDescription, DialogTitle, DialogTrigger } from \"@/components/ballmac/dialog\"\nimport { cn } from \"@/lib/utils\"\nimport { useMessages } from \"@/lib/ballmac/i18n\"\n\ntype VideoDialogProps = Omit<React.ComponentProps<\"button\">, \"title\" | \"children\"> & {\n  /** Name of the video. Read by screen readers and used as the frame's title. */\n  title: string\n  /** A video file to play with the browser's own player. */\n  src?: string\n  /** A page to embed instead, such as a Vimeo player URL. */\n  embedUrl?: string\n  /** A YouTube video id. Plays from the privacy-friendly youtube-nocookie.com domain. */\n  youtubeId?: string\n  /** Image shown on the button. */\n  thumbnail?: string\n  /** Describes the thumbnail for screen readers. Leave empty when the title already says it. */\n  thumbnailAlt?: string\n  /** Length shown on the button, such as \"2:41\". */\n  duration?: string\n  /** Shape of the video frame. */\n  aspect?: \"video\" | \"square\" | \"cinema\"\n  /** Controlled open state. */\n  open?: boolean\n  /** Called when the dialog opens or closes. */\n  onOpenChange?: (open: boolean) => void\n  /** Classes for the thumbnail button. */\n  className?: string\n}\n\nconst ASPECT = { video: \"aspect-video\", square: \"aspect-square\", cinema: \"aspect-[21/9]\" }\n\nfunction embedFor({ embedUrl, youtubeId }: Pick<VideoDialogProps, \"embedUrl\" | \"youtubeId\">) {\n  if (youtubeId) return `https://www.youtube-nocookie.com/embed/${encodeURIComponent(youtubeId)}?autoplay=1&rel=0&modestbranding=1`\n  return embedUrl\n}\n\nfunction VideoDialog({ title, src, embedUrl, youtubeId, thumbnail, thumbnailAlt = \"\", duration, aspect = \"video\", open, onOpenChange, className, ...props }: VideoDialogProps) {\n  const msg = useMessages()\n  const embed = embedFor({ embedUrl, youtubeId })\n  const [loaded, setLoaded] = React.useState(false)\n\n  return (\n    <Dialog open={open} onOpenChange={(next) => { if (!next) setLoaded(false); onOpenChange?.(next) }}>\n      <DialogTrigger\n        data-slot=\"video-dialog-trigger\"\n        className={cn(\n          \"group/video relative block w-full overflow-hidden rounded-2xl border bg-muted text-start outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n          ASPECT[aspect],\n          className\n        )}\n        {...props}\n      >\n        {thumbnail ? (\n          // eslint-disable-next-line @next/next/no-img-element\n          <img src={thumbnail} alt={thumbnailAlt} className=\"absolute inset-0 size-full object-cover transition-transform duration-500 ease-out group-hover/video:scale-[1.03] motion-reduce:transition-none motion-reduce:group-hover/video:scale-100\" />\n        ) : (\n          <span aria-hidden=\"true\" className=\"absolute inset-0 bg-[radial-gradient(120%_120%_at_20%_10%,color-mix(in_oklab,var(--chart-1)_35%,var(--card)),var(--card))]\" />\n        )}\n        <span aria-hidden=\"true\" className=\"absolute inset-0 bg-gradient-to-t from-black/55 via-black/10 to-transparent\" />\n        <span className=\"absolute inset-0 flex items-center justify-center\">\n          <span aria-hidden=\"true\" className=\"relative flex size-16 items-center justify-center rounded-full bg-white/95 text-black shadow-[0_8px_30px_rgb(0_0_0/0.35)] transition-transform duration-200 group-hover/video:scale-110 group-focus-visible/video:scale-110 motion-reduce:transition-none sm:size-20\">\n            <span className=\"absolute inset-0 rounded-full bg-white/70 opacity-0 group-hover/video:animate-ping motion-reduce:animate-none\" />\n            <Play className=\"relative ms-1 size-6 fill-current sm:size-7\" />\n          </span>\n        </span>\n        <span className=\"absolute inset-x-0 bottom-0 flex items-end justify-between gap-3 p-4 text-white\">\n          <span className=\"text-sm font-medium drop-shadow-sm sm:text-base\">\n            <span className=\"sr-only\">{msg(\"video-dialog.playVideo\", \"Play video: {title}\", { title })}</span> <span aria-hidden=\"true\">{title}</span>\n          </span>\n          {duration && <span className=\"rounded-md bg-black/60 px-1.5 py-0.5 font-mono text-xs tabular-nums backdrop-blur\">{duration}</span>}\n        </span>\n      </DialogTrigger>\n      <DialogContent showCloseButton={false} className=\"w-[min(64rem,calc(100%-1.5rem))] max-w-none gap-0 overflow-hidden border-0 bg-black p-0 text-white sm:max-w-5xl sm:p-0\">\n        <DialogTitle className=\"sr-only\">{title}</DialogTitle>\n        <DialogDescription className=\"sr-only\">{msg(\"video-dialog.videoPlayerPressEscapeTo\", \"Video player. Press Escape to close.\")}</DialogDescription>\n        <div className={cn(\"relative w-full bg-black\", ASPECT[aspect])}>\n          {!loaded && <span aria-hidden=\"true\" className=\"absolute inset-0 animate-pulse bg-white/5 motion-reduce:animate-none\" />}\n          {src ? (\n            <video src={src} poster={thumbnail} controls autoPlay playsInline onLoadedData={() => setLoaded(true)} className=\"absolute inset-0 size-full\">\n              <track kind=\"captions\" />\n            </video>\n          ) : embed ? (\n            <iframe\n              src={embed}\n              title={title}\n              allow=\"autoplay; encrypted-media; picture-in-picture; fullscreen\"\n              allowFullScreen\n              referrerPolicy=\"strict-origin-when-cross-origin\"\n              onLoad={() => setLoaded(true)}\n              className=\"absolute inset-0 size-full border-0\"\n            />\n          ) : null}\n        </div>\n        <DialogClose\n          aria-label={msg(\"video-dialog.closeVideo\", \"Close video\")}\n          className=\"absolute top-2.5 end-2.5 inline-flex size-9 items-center justify-center rounded-full bg-black/60 text-white outline-none backdrop-blur transition-colors hover:bg-black/80 focus-visible:ring-[3px] focus-visible:ring-white/60\"\n        >\n          <X aria-hidden=\"true\" className=\"size-4\" />\n        </DialogClose>\n      </DialogContent>\n    </Dialog>\n  )\n}\n\nexport { VideoDialog, type VideoDialogProps }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/video-dialog.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "video",
      "dialog",
      "modal",
      "youtube",
      "thumbnail"
    ],
    "version": "1.0.0",
    "updated": "2026-10-01",
    "ai": {
      "summary": "<VideoDialog title thumbnail duration youtubeId | embedUrl | src aspect />. The player is created on open and removed on close, so nothing loads or keeps playing until asked.",
      "whenToUse": [
        "Product demos and launch videos on landing pages",
        "Tutorial thumbnails"
      ],
      "whenNotToUse": [
        "Inline background video",
        "Audio only (use an audio player)"
      ],
      "composesWith": [
        "dialog",
        "card"
      ],
      "a11y": [
        {
          "keys": "Enter / Space",
          "action": "Opens the video"
        },
        {
          "keys": "Escape",
          "action": "Closes it and returns focus to the thumbnail"
        },
        {
          "keys": "Screen readers",
          "action": "Button reads 'Play video: Title'; the dialog is named by the title"
        }
      ],
      "customization": [
        "youtubeId, embedUrl or src",
        "thumbnail",
        "duration",
        "aspect",
        "open / onOpenChange"
      ]
    },
    "examples": [
      "video-dialog-demo",
      "video-dialog-aspects"
    ],
    "url": "https://ui.ballmac.com/components/video-dialog"
  },
  "categories": [
    "marketing"
  ],
  "type": "registry:ui"
}