{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "elements-media-player",
  "type": "registry:component",
  "title": "Media player",
  "description": "Audio and video a tool returned, played inline without leaving the thread.",
  "registryDependencies": [
    "https://r.assistant-ui.com/elements-surfaces.json"
  ],
  "dependencies": [
    "lucide-react"
  ],
  "files": [
    {
      "type": "registry:component",
      "path": "components/assistant-ui/elements/media-player.tsx",
      "sourcePath": "packages/ui/src/components/react/assistant-ui/elements/media-player.tsx",
      "content": "\"use client\";\n\nimport {\n  useEffect,\n  useRef,\n  useState,\n  type ComponentProps,\n  type RefObject,\n} from \"react\";\nimport { PauseIcon, PlayIcon } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { field, inkButton, mono, paper } from \"./surfaces\";\n\nexport interface AudioPlayerProps extends Omit<\n  ComponentProps<\"div\">,\n  \"children\"\n> {\n  src: string;\n  title?: string | undefined;\n  artwork?: string | undefined;\n  durationMs?: number | undefined;\n}\n\nexport interface VideoPlayerProps extends Omit<\n  ComponentProps<\"div\">,\n  \"children\"\n> {\n  src: string;\n  poster?: string | undefined;\n  title?: string | undefined;\n  ratio?: \"16:9\" | \"4:3\" | \"1:1\" | \"9:16\" | \"auto\" | undefined;\n  durationMs?: number | undefined;\n}\n\nfunction formatDuration(seconds: number): string {\n  const wholeSeconds = Math.max(0, Math.floor(seconds));\n  const minutes = Math.floor(wholeSeconds / 60);\n  const remainingSeconds = wholeSeconds % 60;\n  return `${minutes}:${remainingSeconds.toString().padStart(2, \"0\")}`;\n}\n\nfunction useMediaDuration(\n  mediaRef: RefObject<HTMLMediaElement | null>,\n  src: string,\n  durationMs?: number | undefined,\n) {\n  const [metadata, setMetadata] = useState<{ src: string; duration?: number }>({\n    src,\n  });\n\n  // A server-rendered media element can load metadata before hydration attaches onLoadedMetadata.\n  useEffect(() => {\n    const media = mediaRef.current;\n    if (!media || media.readyState < HTMLMediaElement.HAVE_METADATA) return;\n    if (Number.isFinite(media.duration) && media.duration >= 0) {\n      setMetadata({ src, duration: media.duration });\n    }\n  }, [mediaRef, src]);\n  const fallbackDuration =\n    durationMs !== undefined && Number.isFinite(durationMs)\n      ? Math.max(0, durationMs / 1000)\n      : undefined;\n\n  return {\n    duration:\n      metadata.src === src\n        ? (metadata.duration ?? fallbackDuration)\n        : fallbackDuration,\n    setDurationFromMetadata: (duration: number) => {\n      if (Number.isFinite(duration) && duration >= 0) {\n        setMetadata({ src, duration });\n      }\n    },\n  };\n}\n\nexport function AudioPlayer({\n  src,\n  title,\n  artwork,\n  durationMs,\n  className,\n  ...props\n}: AudioPlayerProps) {\n  const audioRef = useRef<HTMLAudioElement>(null);\n  const [playback, setPlayback] = useState({\n    src,\n    currentTime: 0,\n    playing: false,\n    hasError: false,\n  });\n  const { duration, setDurationFromMetadata } = useMediaDuration(\n    audioRef,\n    src,\n    durationMs,\n  );\n  const playbackState =\n    playback.src === src\n      ? playback\n      : { src, currentTime: 0, playing: false, hasError: false };\n  const { currentTime, playing, hasError } = playbackState;\n  const update = (patch: Partial<Omit<typeof playback, \"src\">>) =>\n    setPlayback((current) => ({\n      ...(current.src === src\n        ? current\n        : { src, currentTime: 0, playing: false, hasError: false }),\n      ...patch,\n    }));\n  const displayTitle = title ?? \"Audio\";\n  const playbackLabel = title ? ` ${title}` : \" audio\";\n  const durationLabel =\n    duration === undefined ? \"--:--\" : formatDuration(duration);\n\n  useEffect(() => {\n    if (audioRef.current?.error) {\n      setPlayback({ src, currentTime: 0, playing: false, hasError: true });\n    }\n  }, [src]);\n\n  const togglePlayback = () => {\n    const audio = audioRef.current;\n    if (!audio || hasError) return;\n\n    if (playing) {\n      audio.pause();\n      update({ playing: false });\n      return;\n    }\n\n    void audio.play().then(\n      () => update({ playing: true }),\n      () =>\n        update(\n          audio.error ? { hasError: true, playing: false } : { playing: false },\n        ),\n    );\n  };\n\n  return (\n    <div\n      data-slot=\"audio-player\"\n      className={cn(\n        paper,\n        \"flex w-full max-w-md items-center gap-3 rounded-2xl p-3\",\n        className,\n      )}\n      {...props}\n    >\n      {artwork ? (\n        <img\n          src={artwork}\n          alt=\"\"\n          className=\"size-10 shrink-0 rounded-lg object-cover\"\n        />\n      ) : null}\n      <button\n        type=\"button\"\n        aria-label={`${playing ? \"Pause\" : \"Play\"}${playbackLabel}`}\n        disabled={hasError}\n        onClick={togglePlayback}\n        className={cn(\n          inkButton,\n          \"flex size-9 shrink-0 items-center justify-center rounded-full disabled:cursor-default disabled:opacity-40\",\n        )}\n      >\n        {playing ? (\n          <PauseIcon aria-hidden className=\"size-4\" />\n        ) : (\n          <PlayIcon aria-hidden className=\"ml-0.5 size-4\" />\n        )}\n      </button>\n      <div className=\"flex min-w-0 flex-1 flex-col gap-1.5\">\n        <span className=\"truncate text-[13.5px] font-medium\">\n          {displayTitle}\n        </span>\n        <input\n          type=\"range\"\n          min={0}\n          max={duration ?? 0}\n          step={0.1}\n          value={Math.min(currentTime, duration ?? 0)}\n          aria-label=\"Seek\"\n          aria-valuetext={`${formatDuration(currentTime)} of ${durationLabel}`}\n          disabled={hasError}\n          onChange={(event) => {\n            const nextTime = Number(event.currentTarget.value);\n            if (!Number.isFinite(nextTime)) return;\n            const resolvedTime = Math.min(nextTime, duration ?? nextTime);\n            if (audioRef.current) audioRef.current.currentTime = resolvedTime;\n            update({ currentTime: resolvedTime });\n          }}\n          className=\"accent-foreground w-full cursor-pointer disabled:cursor-default\"\n        />\n      </div>\n      <span className={cn(mono, \"text-foreground/45 shrink-0 tabular-nums\")}>\n        {formatDuration(currentTime)} / {durationLabel}\n      </span>\n      <audio\n        ref={audioRef}\n        src={src}\n        preload=\"metadata\"\n        className=\"hidden\"\n        onLoadedMetadata={(event) => {\n          setDurationFromMetadata(event.currentTarget.duration);\n        }}\n        onTimeUpdate={(event) =>\n          update({ currentTime: event.currentTarget.currentTime })\n        }\n        onPlay={() => update({ playing: true })}\n        onPause={() => update({ playing: false })}\n        onError={() => update({ hasError: true, playing: false })}\n      />\n      {hasError ? (\n        <span role=\"alert\" className=\"text-foreground/45 shrink-0 text-xs\">\n          Can't play this audio\n        </span>\n      ) : null}\n    </div>\n  );\n}\n\nexport function VideoPlayer({\n  src,\n  poster,\n  title,\n  ratio = \"16:9\",\n  durationMs,\n  className,\n  ...props\n}: VideoPlayerProps) {\n  const videoRef = useRef<HTMLVideoElement>(null);\n  const { duration, setDurationFromMetadata } = useMediaDuration(\n    videoRef,\n    src,\n    durationMs,\n  );\n  const ratioClassName =\n    ratio === \"1:1\"\n      ? \"aspect-square\"\n      : ratio === \"4:3\"\n        ? \"aspect-[4/3]\"\n        : ratio === \"16:9\"\n          ? \"aspect-video\"\n          : ratio === \"9:16\"\n            ? \"aspect-[9/16]\"\n            : undefined;\n\n  return (\n    <div\n      data-slot=\"video-player\"\n      className={cn(\"flex w-full max-w-xl flex-col gap-2\", className)}\n      {...props}\n    >\n      <div className={cn(field, \"overflow-hidden rounded-xl\", ratioClassName)}>\n        <video\n          ref={videoRef}\n          src={src}\n          poster={poster}\n          controls\n          playsInline\n          preload=\"metadata\"\n          aria-label={title ?? \"Video\"}\n          className={cn(\n            \"block max-w-full\",\n            ratio !== \"auto\" && \"h-full w-full object-contain\",\n          )}\n          onLoadedMetadata={(event) => {\n            setDurationFromMetadata(event.currentTarget.duration);\n          }}\n        />\n      </div>\n      {(title || duration !== undefined) && (\n        <div className=\"flex min-w-0 items-center gap-2 px-1\">\n          {title ? (\n            <span className=\"min-w-0 flex-1 truncate text-[13.5px] font-medium\">\n              {title}\n            </span>\n          ) : null}\n          {duration !== undefined ? (\n            <span\n              className={cn(mono, \"text-foreground/45 shrink-0 tabular-nums\")}\n            >\n              {formatDuration(duration)}\n            </span>\n          ) : null}\n        </div>\n      )}\n    </div>\n  );\n}\n"
    }
  ]
}