{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "media-player",
  "title": "Media Player",
  "description": "A styled media player shell — a poster with a centred featured-icon play overlay, a native video wrapped in a UUI-styled control bar (scrubber, time, volume, fullscreen), and a MediaThumbnail sibling that opens the film in a dialog.",
  "dependencies": [
    "@untitledui/icons@^0.0.22"
  ],
  "registryDependencies": [
    "@nx-ui/dialog",
    "@nx-ui/featured-icon",
    "@nx-ui/utils"
  ],
  "files": [
    {
      "path": "components/blocks/marketing/media-player.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  Maximize01,\n  PauseCircle,\n  Play,\n  PlayCircle,\n  VolumeMax,\n  VolumeX,\n} from \"@untitledui/icons\"\n\nimport { cn } from \"@/lib/utils\"\nimport { FeaturedIcon } from \"@/components/blocks/featured-icon\"\nimport {\n  Dialog,\n  DialogContent,\n  DialogTitle,\n  DialogTrigger,\n} from \"@/components/ui/dialog\"\n\n/**\n * MediaPlayer — a styled shell around a native `<video>`: a poster frame with a\n * centred, featured-icon play overlay that reveals the player, then a UUI-styled\n * control bar (play/pause, a token-tinted scrubber, elapsed / total time, mute +\n * volume, and fullscreen). Motion is gentle and gated on\n * `prefers-reduced-motion`.\n *\n * `MediaThumbnail` is the lightweight sibling: a poster tile with a play badge\n * that opens the same video inside a `Dialog` — the pattern for embedding a\n * \"watch the film\" cue inside dense marketing copy.\n */\n\nfunction formatTime(seconds: number) {\n  if (!Number.isFinite(seconds) || seconds < 0) seconds = 0\n  const m = Math.floor(seconds / 60)\n  const s = Math.floor(seconds % 60)\n  return `${m}:${s.toString().padStart(2, \"0\")}`\n}\n\nconst scrubberClass =\n  \"h-1.5 w-full cursor-pointer appearance-none rounded-full bg-transparent outline-none focus-visible:ring-4 focus-visible:ring-ring/25 [&::-webkit-slider-thumb]:size-3.5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:shadow-md [&::-moz-range-thumb]:size-3.5 [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-0 [&::-moz-range-thumb]:bg-white\"\n\ninterface VideoControlsProps {\n  src: string\n  poster?: string\n  /** Autoplay (muted) once mounted — used by the dialog variant. */\n  autoPlay?: boolean\n  className?: string\n}\n\n/** The bare styled `<video>` + control bar, with no poster overlay. */\nfunction VideoSurface({ src, poster, autoPlay, className }: VideoControlsProps) {\n  const containerRef = React.useRef<HTMLDivElement>(null)\n  const videoRef = React.useRef<HTMLVideoElement>(null)\n  const [playing, setPlaying] = React.useState(false)\n  const [muted, setMuted] = React.useState(Boolean(autoPlay))\n  const [volume, setVolume] = React.useState(1)\n  const [current, setCurrent] = React.useState(0)\n  const [duration, setDuration] = React.useState(0)\n\n  const togglePlay = React.useCallback(() => {\n    const video = videoRef.current\n    if (!video) return\n    if (video.paused) void video.play()\n    else video.pause()\n  }, [])\n\n  const toggleMute = React.useCallback(() => {\n    const video = videoRef.current\n    if (!video) return\n    video.muted = !video.muted\n    setMuted(video.muted)\n  }, [])\n\n  const onSeek = (event: React.ChangeEvent<HTMLInputElement>) => {\n    const video = videoRef.current\n    if (!video) return\n    const next = Number(event.target.value)\n    video.currentTime = next\n    setCurrent(next)\n  }\n\n  const onVolume = (event: React.ChangeEvent<HTMLInputElement>) => {\n    const video = videoRef.current\n    if (!video) return\n    const next = Number(event.target.value)\n    video.volume = next\n    video.muted = next === 0\n    setVolume(next)\n    setMuted(next === 0)\n  }\n\n  const toggleFullscreen = React.useCallback(() => {\n    const el = containerRef.current\n    if (!el) return\n    if (document.fullscreenElement) void document.exitFullscreen()\n    else void el.requestFullscreen?.()\n  }, [])\n\n  const progress = duration > 0 ? (current / duration) * 100 : 0\n  const volumePercent = (muted ? 0 : volume) * 100\n\n  return (\n    <div\n      ref={containerRef}\n      data-slot=\"media-surface\"\n      className={cn(\n        \"group/media relative isolate overflow-hidden rounded-2xl bg-black ring-1 ring-border-secondary\",\n        className\n      )}\n    >\n      {/* eslint-disable-next-line jsx-a11y/media-has-caption */}\n      <video\n        ref={videoRef}\n        src={src}\n        poster={poster}\n        autoPlay={autoPlay}\n        muted={autoPlay}\n        playsInline\n        onClick={togglePlay}\n        onPlay={() => setPlaying(true)}\n        onPause={() => setPlaying(false)}\n        onTimeUpdate={(e) => setCurrent(e.currentTarget.currentTime)}\n        onLoadedMetadata={(e) => setDuration(e.currentTarget.duration)}\n        className=\"aspect-video w-full cursor-pointer object-cover\"\n      />\n\n      <div className=\"pointer-events-none absolute inset-x-0 bottom-0 flex flex-col gap-2 bg-gradient-to-t from-black/80 via-black/30 to-transparent p-4 pt-10 opacity-0 transition-opacity duration-200 ease-linear group-hover/media:opacity-100 group-focus-within/media:opacity-100 data-[playing=false]:opacity-100\">\n        <div className=\"pointer-events-auto flex flex-col gap-2\" data-playing={playing}>\n          <input\n            type=\"range\"\n            min={0}\n            max={duration || 0}\n            step=\"any\"\n            value={current}\n            onChange={onSeek}\n            aria-label=\"Seek\"\n            className={scrubberClass}\n            style={{\n              backgroundImage: `linear-gradient(to right, var(--color-accent-400) ${progress}%, rgba(255,255,255,0.28) ${progress}%)`,\n            }}\n          />\n          <div className=\"flex items-center gap-3 text-white\">\n            <button\n              type=\"button\"\n              onClick={togglePlay}\n              aria-label={playing ? \"Pause\" : \"Play\"}\n              className=\"flex size-9 cursor-pointer items-center justify-center rounded-full text-white transition duration-150 ease-linear outline-none hover:bg-white/15 focus-visible:ring-4 focus-visible:ring-white/30\"\n            >\n              {playing ? (\n                <PauseCircle className=\"size-5\" />\n              ) : (\n                <PlayCircle className=\"size-5\" />\n              )}\n            </button>\n\n            <div className=\"flex items-center gap-2\">\n              <button\n                type=\"button\"\n                onClick={toggleMute}\n                aria-label={muted ? \"Unmute\" : \"Mute\"}\n                className=\"flex size-9 cursor-pointer items-center justify-center rounded-full text-white transition duration-150 ease-linear outline-none hover:bg-white/15 focus-visible:ring-4 focus-visible:ring-white/30\"\n              >\n                {muted ? (\n                  <VolumeX className=\"size-5\" />\n                ) : (\n                  <VolumeMax className=\"size-5\" />\n                )}\n              </button>\n              <input\n                type=\"range\"\n                min={0}\n                max={1}\n                step={0.05}\n                value={muted ? 0 : volume}\n                onChange={onVolume}\n                aria-label=\"Volume\"\n                className={cn(scrubberClass, \"hidden w-20 sm:block\")}\n                style={{\n                  backgroundImage: `linear-gradient(to right, #fff ${volumePercent}%, rgba(255,255,255,0.28) ${volumePercent}%)`,\n                }}\n              />\n            </div>\n\n            <span className=\"text-xs font-medium tabular-nums text-white/80\">\n              {formatTime(current)} / {formatTime(duration)}\n            </span>\n\n            <button\n              type=\"button\"\n              onClick={toggleFullscreen}\n              aria-label=\"Toggle fullscreen\"\n              className=\"ml-auto flex size-9 cursor-pointer items-center justify-center rounded-full text-white transition duration-150 ease-linear outline-none hover:bg-white/15 focus-visible:ring-4 focus-visible:ring-white/30\"\n            >\n              <Maximize01 className=\"size-5\" />\n            </button>\n          </div>\n        </div>\n      </div>\n    </div>\n  )\n}\n\ninterface MediaPlayerProps {\n  src: string\n  poster?: string\n  /** Optional eyebrow + title shown across the poster before playback starts. */\n  title?: React.ReactNode\n  caption?: React.ReactNode\n  className?: string\n}\n\nfunction MediaPlayer({\n  src,\n  poster,\n  title,\n  caption,\n  className,\n}: MediaPlayerProps) {\n  const [started, setStarted] = React.useState(false)\n\n  if (started) {\n    return <VideoSurface src={src} poster={poster} autoPlay className={className} />\n  }\n\n  return (\n    <div\n      data-slot=\"media-player\"\n      className={cn(\n        \"group/poster relative isolate aspect-video overflow-hidden rounded-2xl bg-black ring-1 ring-border-secondary\",\n        className\n      )}\n    >\n      {poster ? (\n        // eslint-disable-next-line @next/next/no-img-element\n        <img\n          src={poster}\n          alt=\"\"\n          className=\"absolute inset-0 size-full object-cover\"\n        />\n      ) : null}\n      <div\n        aria-hidden=\"true\"\n        className=\"absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-black/10\"\n      />\n      <button\n        type=\"button\"\n        onClick={() => setStarted(true)}\n        aria-label=\"Play video\"\n        className=\"absolute inset-0 flex cursor-pointer flex-col items-center justify-center gap-5 outline-none focus-visible:ring-4 focus-visible:ring-white/40\"\n      >\n        <FeaturedIcon\n          icon={Play}\n          theme=\"modern\"\n          size=\"xl\"\n          className=\"transition duration-200 ease-out group-hover/poster:scale-110 motion-reduce:transition-none motion-reduce:group-hover/poster:scale-100\"\n        />\n        {title ? (\n          <span className=\"max-w-md px-6 text-center text-lg font-semibold text-white text-balance md:text-xl\">\n            {title}\n          </span>\n        ) : null}\n      </button>\n      {caption ? (\n        <span className=\"absolute bottom-4 left-5 text-sm font-medium text-white/70\">\n          {caption}\n        </span>\n      ) : null}\n    </div>\n  )\n}\n\ninterface MediaThumbnailProps {\n  src: string\n  poster?: string\n  /** Dialog title (also the accessible label). */\n  title?: React.ReactNode\n  /** Caption shown beneath the play badge on the tile. */\n  label?: React.ReactNode\n  className?: string\n}\n\n/** A poster tile with a play badge that opens the video in a `Dialog`. */\nfunction MediaThumbnail({\n  src,\n  poster,\n  title = \"Watch the film\",\n  label,\n  className,\n}: MediaThumbnailProps) {\n  return (\n    <Dialog>\n      <DialogTrigger\n        data-slot=\"media-thumbnail\"\n        className={cn(\n          \"group/thumb relative isolate flex aspect-video w-full cursor-pointer items-center justify-center overflow-hidden rounded-2xl bg-black ring-1 ring-border-secondary outline-none focus-visible:ring-4 focus-visible:ring-ring/25\",\n          className\n        )}\n      >\n        {poster ? (\n          // eslint-disable-next-line @next/next/no-img-element\n          <img\n            src={poster}\n            alt=\"\"\n            className=\"absolute inset-0 size-full object-cover transition duration-300 ease-out group-hover/thumb:scale-105 motion-reduce:transition-none motion-reduce:group-hover/thumb:scale-100\"\n          />\n        ) : null}\n        <span\n          aria-hidden=\"true\"\n          className=\"absolute inset-0 bg-gradient-to-t from-black/70 via-black/10 to-transparent\"\n        />\n        <span className=\"relative flex flex-col items-center gap-3\">\n          <FeaturedIcon icon={Play} theme=\"modern\" size=\"lg\" />\n          {label ? (\n            <span className=\"text-sm font-semibold text-white\">{label}</span>\n          ) : null}\n        </span>\n      </DialogTrigger>\n      <DialogContent\n        showCloseButton\n        className=\"max-w-3xl overflow-hidden bg-black p-0 ring-foreground/20 sm:max-w-3xl\"\n      >\n        <DialogTitle className=\"sr-only\">{title}</DialogTitle>\n        <VideoSurface src={src} poster={poster} autoPlay className=\"rounded-none ring-0\" />\n      </DialogContent>\n    </Dialog>\n  )\n}\n\nexport { MediaPlayer, MediaThumbnail }\n",
      "type": "registry:component",
      "target": "components/blocks/marketing/media-player.tsx"
    }
  ],
  "type": "registry:block"
}