{"version":3,"file":"AttachmentCard-DUrgGRsq.cjs","names":[],"sources":["../src/utils/cdnImageUrl.ts","../src/utils/nativeMediaPlayers.ts","../src/components/AttachmentCard/utils/mimeType.ts","../src/components/AttachmentCard/utils/icons.ts","../src/utils/isDevBuild.ts","../src/components/AttachmentCard/MediaPlayer.tsx","../src/components/AttachmentCard/Thumbnail.tsx"],"sourcesContent":["const LINKTREE_CDN_HOSTS = ['linktr.ee']\n\nconst ATTACHMENT_IMAGE_SIZE_PRESET = 'messaging-attachment-v1_0'\n\nfunction hostnameFromUrl(url: string): string | null {\n  try {\n    return new URL(url).hostname.toLowerCase()\n  } catch {\n    return null\n  }\n}\n\nfunction isLinktreeCdnHost(hostname: string): boolean {\n  return LINKTREE_CDN_HOSTS.some(\n    (host) => hostname === host || hostname.endsWith(`.${host}`)\n  )\n}\n\nexport function optimizeMessagingAttachmentUrl(\n  url: string | undefined | null\n): string | undefined {\n  const trimmed = url?.trim()\n  if (!trimmed) return undefined\n\n  const hostname = hostnameFromUrl(trimmed)\n  if (!hostname || !isLinktreeCdnHost(hostname)) return trimmed\n\n  const parsedUrl = new URL(trimmed)\n  const isAlreadyMessagingOptimized =\n    parsedUrl.searchParams.get('io') === 'true' &&\n    parsedUrl.searchParams.get('size') === ATTACHMENT_IMAGE_SIZE_PRESET\n\n  if (isAlreadyMessagingOptimized) {\n    return trimmed\n  }\n\n  parsedUrl.searchParams.set('io', 'true')\n  parsedUrl.searchParams.set('size', ATTACHMENT_IMAGE_SIZE_PRESET)\n\n  return parsedUrl.toString()\n}\n","/**\n * Attribute marking every native `<audio>` / `<video>` the toolkit\n * renders on a message surface, so opening one player can silence the\n * others.\n *\n * stream-chat-react's `AudioPlayerPool` — what `useActiveAudioPlayer()`\n * reads — only knows the players *it* mounts, i.e. voice recordings.\n * Everything the toolkit draws itself is a plain native element that\n * never registers there: `MessageAttachment.Audio`, a playable\n * `LinkAttachment` card, and `AttachmentCard`'s `MediaPlayer`. Pausing\n * the pool alone therefore left thread audio sounding underneath an\n * opened video viewer on the production `StreamAttachmentMessage` path,\n * which is exactly the surface the mutual exclusion exists for\n * (MES-1353).\n */\nexport const NATIVE_PLAYER_ATTRIBUTE = 'data-mes-native-player'\n\n/**\n * Spread onto a native player element to enrol it. Keyed off the\n * constant so the marker and the query below can't drift apart.\n */\nexport const nativePlayerProps = { [NATIVE_PLAYER_ATTRIBUTE]: '' } as const\n\n/**\n * Pauses every enrolled player except `except`.\n *\n * No `paused` check: pausing an already-paused element is a no-op per\n * spec, and reading `paused` first would only add a branch that jsdom\n * answers differently from a browser.\n */\nexport const pauseNativeMediaPlayers = (\n  except?: HTMLMediaElement | null\n): void => {\n  if (typeof document === 'undefined') return\n\n  document\n    .querySelectorAll<HTMLMediaElement>(`[${NATIVE_PLAYER_ATTRIBUTE}]`)\n    .forEach((player) => {\n      if (player !== except) player.pause()\n    })\n}\n","export type AttachmentSourceType = 'image' | 'audio' | 'video' | 'document'\n\nexport type DocumentIconType =\n  | 'pdf'\n  | 'doc'\n  | 'xls'\n  | 'csv'\n  | 'ppt'\n  | 'zip'\n  | 'text'\n  | 'markdown'\n  | 'generic'\n\nconst DOCUMENT_ICON_PATTERNS: Array<[RegExp, DocumentIconType]> = [\n  [/pdf/, 'pdf'],\n  [/wordprocessingml|msword|\\.doc/, 'doc'],\n  [/spreadsheetml|ms-excel|\\.xls/, 'xls'],\n  [/csv/, 'csv'],\n  [/presentationml|ms-powerpoint|\\.ppt/, 'ppt'],\n  [/zip|x-rar|x-7z|x-tar|x-gzip/, 'zip'],\n  [/plain|rtf/, 'text'],\n  [/markdown/, 'markdown'],\n]\n\nexport function getSourceType(mimeType: string): AttachmentSourceType {\n  if (mimeType.startsWith('video/')) return 'video'\n  if (mimeType.startsWith('audio/')) return 'audio'\n  if (mimeType.startsWith('image/')) return 'image'\n  return 'document'\n}\n\nexport function getDocumentIconType(mimeType: string): DocumentIconType {\n  const match = DOCUMENT_ICON_PATTERNS.find(([pattern]) =>\n    pattern.test(mimeType)\n  )\n  return match ? match[1] : 'generic'\n}\n","import {\n  FileIcon,\n  FileCsvIcon,\n  FileDocIcon,\n  FileMdIcon,\n  FilePdfIcon,\n  FilePptIcon,\n  FileTextIcon,\n  FileXlsIcon,\n  FileZipIcon,\n  ImageIcon,\n  SpeakerHighIcon,\n  VideoCameraIcon,\n  IconProps,\n} from '@phosphor-icons/react'\nimport React from 'react'\n\nimport { getDocumentIconType, getSourceType } from './mimeType'\nimport type { AttachmentSourceType } from './mimeType'\n\nconst MEDIA_TYPE_ICON: Record<AttachmentSourceType, React.ElementType> = {\n  video: VideoCameraIcon,\n  audio: SpeakerHighIcon,\n  image: ImageIcon,\n  document: FileIcon,\n}\n\nconst DOCUMENT_ICON_COMPONENT = {\n  pdf: FilePdfIcon,\n  doc: FileDocIcon,\n  xls: FileXlsIcon,\n  csv: FileCsvIcon,\n  ppt: FilePptIcon,\n  zip: FileZipIcon,\n  text: FileTextIcon,\n  markdown: FileMdIcon,\n  generic: FileIcon,\n} as const\n\nfunction getTypeIcon(mimeType: string): React.ElementType {\n  const sourceType = getSourceType(mimeType)\n  if (sourceType !== 'document') return MEDIA_TYPE_ICON[sourceType]\n  return DOCUMENT_ICON_COMPONENT[getDocumentIconType(mimeType)]\n}\n\n/** Use instead of `<TypeIcon />` where TypeIcon = getTypeIcon(mime) to satisfy react-hooks/static-components. */\nexport function renderTypeIcon(\n  mimeType: string,\n  props: IconProps\n): React.ReactElement {\n  return React.createElement(getTypeIcon(mimeType), props)\n}\n","/**\n * True in Vite dev builds (`import.meta.env.DEV`). Uses a type assertion so\n * `tsc` does not rely on ambient `ImportMeta` merging (vite/client).\n */\nexport function isDevBuild(): boolean {\n  return (\n    typeof import.meta !== 'undefined' &&\n    (import.meta as unknown as { env?: { DEV?: boolean } }).env?.DEV === true\n  )\n}\n","import { CircleNotchIcon, PauseIcon, PlayIcon } from '@phosphor-icons/react'\nimport React, { useCallback, useEffect, useRef, useState } from 'react'\n\nimport { isDevBuild } from '../../utils/isDevBuild'\nimport { nativePlayerProps } from '../../utils/nativeMediaPlayers'\n\nimport { renderTypeIcon } from './utils/icons'\nimport { getSourceType } from './utils/mimeType'\n\ntype TouchEventUnion =\n  MouseEvent | TouchEvent | React.MouseEvent | React.TouchEvent\n\nconst getClientXFromEvent = (e: TouchEventUnion): number => {\n  if ('touches' in e) {\n    return e.touches[0]?.clientX ?? e.changedTouches[0]?.clientX ?? 0\n  }\n  return e.clientX\n}\n\nexport interface MediaPlayerProps {\n  source: string\n  mimeType: string\n  poster?: string\n  autoPlay?: boolean\n  /** Controlled playing state. When provided, syncs to internal play/pause. */\n  playing?: boolean\n  loop?: boolean\n  controls?: boolean\n  showProgress?: boolean\n  /** When true, requests muted playback (helps autoplay policies on video). */\n  muted?: boolean\n  /**\n   * When provided, overrides the default click-to-play-toggle behaviour on the\n   * player container. The play/pause button (which calls stopPropagation) is\n   * unaffected, so inline playback still works.\n   */\n  onContainerClick?: (e: React.MouseEvent) => void\n}\n\nconst MediaPlayer: React.FC<MediaPlayerProps> = ({\n  source,\n  mimeType,\n  poster,\n  autoPlay = false,\n  playing: playingProp,\n  loop = false,\n  controls = true,\n  showProgress = false,\n  muted = false,\n  onContainerClick,\n}) => {\n  // --- Derived ---\n  const sourceType = getSourceType(mimeType)\n\n  // --- Refs ---\n  const playerRef = useRef<HTMLMediaElement>(null)\n  const trackRef = useRef<HTMLDivElement>(null)\n  const rafRef = useRef<number | null>(null)\n  const prevPlayingPropRef = useRef(playingProp)\n\n  // --- State: playback ---\n  const [playing, setPlaying] = useState(autoPlay)\n  const [played, setPlayed] = useState(0)\n  const [seeking, setSeeking] = useState(false)\n\n  // --- State: UI ---\n  const [scrubberHovered, setScrubberHovered] = useState(false)\n  /** Set when autoplay/play() was rejected so user can start via gesture (no controls UI). */\n  const [manualPlayRequired, setManualPlayRequired] = useState(false)\n\n  // --- State: loading ---\n  const [buffering, setBuffering] = useState(false)\n  /** True until the first canPlay fires for the current source — hides controls/spinner behind poster. */\n  const [initialLoad, setInitialLoad] = useState(true)\n  const [videoAspect, setVideoAspect] = useState<number | null>(null)\n\n  // --- Callbacks ---\n  const startPlaybackFromGesture = useCallback(() => {\n    setManualPlayRequired(false)\n    setPlaying(true)\n  }, [])\n\n  const getFraction = useCallback((e: TouchEventUnion) => {\n    const track = trackRef.current\n    if (!track) return 0\n    const rect = track.getBoundingClientRect()\n    return Math.max(\n      0,\n      Math.min(1, (getClientXFromEvent(e) - rect.left) / rect.width)\n    )\n  }, [])\n\n  const seekTo = useCallback((fraction: number) => {\n    const el = playerRef.current\n    if (el && el.duration) el.currentTime = fraction * el.duration\n  }, [])\n\n  const handleTrackPointerDown = (\n    e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>\n  ) => {\n    e.stopPropagation()\n    setSeeking(true)\n    const fraction = getFraction(e)\n    setPlayed(fraction)\n    seekTo(fraction)\n  }\n\n  const handleScrubberKeyDown = useCallback(\n    (e: React.KeyboardEvent<HTMLDivElement>) => {\n      if (e.key === 'ArrowRight') seekTo(Math.min(1, played + 0.05))\n      if (e.key === 'ArrowLeft') seekTo(Math.max(0, played - 0.05))\n    },\n    [seekTo, played]\n  )\n\n  // --- Effects ---\n\n  // Sync controlled playing prop to internal state\n  useEffect(() => {\n    if (\n      playingProp !== undefined &&\n      playingProp !== prevPlayingPropRef.current\n    ) {\n      prevPlayingPropRef.current = playingProp\n      setPlaying(playingProp)\n    }\n  }, [playingProp])\n\n  // RAF-driven progress updates\n  useEffect(() => {\n    if (!playing) {\n      if (rafRef.current !== null) {\n        cancelAnimationFrame(rafRef.current)\n        rafRef.current = null\n      }\n      return\n    }\n    const tick = () => {\n      const el = playerRef.current\n      if (el && el.duration && !seeking) setPlayed(el.currentTime / el.duration)\n      rafRef.current = requestAnimationFrame(tick)\n    }\n    rafRef.current = requestAnimationFrame(tick)\n    return () => {\n      if (rafRef.current !== null) cancelAnimationFrame(rafRef.current)\n    }\n  }, [playing, seeking])\n\n  // ReactPlayer v3 uses native HTML media elements and does not support a\n  // declarative `playing` prop — playback must be driven imperatively.\n  useEffect(() => {\n    const el = playerRef.current\n    if (!el) return\n    if (playing) {\n      void el.play().catch((err) => {\n        setPlaying(false)\n        setManualPlayRequired(true)\n        if (isDevBuild()) {\n          console.debug('[MediaPlayer] play() failed', err)\n        }\n      })\n    } else {\n      el.pause()\n    }\n  }, [playing])\n\n  // Global seeking listeners\n  useEffect(() => {\n    if (!seeking) return\n    const onMove = (e: MouseEvent | TouchEvent) => setPlayed(getFraction(e))\n    const onUp = (e: MouseEvent | TouchEvent) => {\n      setSeeking(false)\n      seekTo(getFraction(e))\n    }\n    window.addEventListener('mousemove', onMove)\n    window.addEventListener('mouseup', onUp)\n    window.addEventListener('touchmove', onMove, { passive: true })\n    window.addEventListener('touchend', onUp)\n    return () => {\n      window.removeEventListener('mousemove', onMove)\n      window.removeEventListener('mouseup', onUp)\n      window.removeEventListener('touchmove', onMove)\n      window.removeEventListener('touchend', onUp)\n    }\n  }, [seeking, getFraction, seekTo])\n\n  // --- Derived render values ---\n  // Use natural aspect ratio once metadata loads, fall back to 16:9 before then.\n  const aspectStyle = videoAspect\n    ? { aspectRatio: String(videoAspect) }\n    : undefined\n  const aspectClass = !videoAspect ? ' aspect-video' : ''\n  const scrubberPercent = Math.round(played * 100)\n\n  return (\n    <div\n      role=\"button\"\n      tabIndex={0}\n      className={`relative cursor-pointer overflow-hidden bg-black ${aspectClass}`}\n      style={aspectStyle}\n      onClick={(e) => {\n        if (onContainerClick) {\n          onContainerClick(e)\n          return\n        }\n        if (manualPlayRequired) return\n        if (controls) setPlaying((p) => !p)\n      }}\n      onKeyDown={(e) => {\n        if (e.key !== 'Enter' && e.key !== ' ') return\n        e.preventDefault()\n        if (onContainerClick) {\n          onContainerClick(e as unknown as React.MouseEvent)\n          return\n        }\n        if (manualPlayRequired) return\n        if (controls) setPlaying((p) => !p)\n      }}\n    >\n      {/* For audio, poster persists as a visual background. For video, hide once loaded. */}\n      {poster && (sourceType === 'audio' || initialLoad) && (\n        <img\n          src={poster}\n          alt=\"\"\n          className=\"absolute inset-0 h-full w-full object-cover\"\n        />\n      )}\n      {!poster && (sourceType === 'audio' || initialLoad) && (\n        <div className=\"absolute inset-0 flex items-center justify-center\">\n          {renderTypeIcon(mimeType, {\n            className: 'size-12 text-black/20',\n            weight: 'regular',\n          })}\n        </div>\n      )}\n      <div className=\"absolute inset-0\">\n        {sourceType === 'audio' ? (\n          <audio\n            {...nativePlayerProps}\n            ref={playerRef as React.RefObject<HTMLAudioElement>}\n            src={source}\n            loop={loop}\n            muted={muted}\n            style={{ width: '100%', height: '100%' }}\n            onLoadStart={() => setBuffering(true)}\n            onCanPlay={() => {\n              setBuffering(false)\n              setInitialLoad(false)\n            }}\n            onWaiting={() => setBuffering(true)}\n            onPlay={() => setManualPlayRequired(false)}\n            onEnded={() => {\n              if (!loop) {\n                setPlaying(false)\n                setPlayed(0)\n              }\n            }}\n          >\n            <track kind=\"captions\" />\n          </audio>\n        ) : (\n          <video\n            ref={playerRef as React.RefObject<HTMLVideoElement>}\n            src={source}\n            loop={loop}\n            muted={muted}\n            playsInline\n            style={{ width: '100%', height: '100%' }}\n            onLoadStart={() => setBuffering(true)}\n            onCanPlay={() => {\n              setBuffering(false)\n              setInitialLoad(false)\n            }}\n            onWaiting={() => setBuffering(true)}\n            onPlay={() => setManualPlayRequired(false)}\n            onLoadedMetadata={() => {\n              const el = playerRef.current\n              if (\n                el instanceof HTMLVideoElement &&\n                el.videoWidth &&\n                el.videoHeight\n              ) {\n                setVideoAspect(el.videoWidth / el.videoHeight)\n              }\n            }}\n            onEnded={() => {\n              if (!loop) {\n                setPlaying(false)\n                setPlayed(0)\n              }\n            }}\n          >\n            <track kind=\"captions\" />\n          </video>\n        )}\n      </div>\n\n      {buffering && !manualPlayRequired && (\n        <div className=\"absolute inset-0 z-10 flex items-center justify-center\">\n          <CircleNotchIcon\n            className=\"size-8 animate-spin text-white/80\"\n            weight=\"bold\"\n          />\n        </div>\n      )}\n\n      {manualPlayRequired && !controls && (\n        <div\n          className=\"absolute inset-0 z-30 flex cursor-pointer items-center justify-center bg-black/35\"\n          role=\"button\"\n          tabIndex={0}\n          aria-label=\"Play preview\"\n          onClick={(e) => {\n            e.stopPropagation()\n            startPlaybackFromGesture()\n          }}\n          onKeyDown={(e) => {\n            if (e.key !== 'Enter' && e.key !== ' ') return\n            e.preventDefault()\n            e.stopPropagation()\n            startPlaybackFromGesture()\n          }}\n        >\n          <span className=\"flex size-16 items-center justify-center rounded-full bg-white/20 text-white backdrop-blur-sm\">\n            <PlayIcon className=\"size-9 translate-x-0.5\" weight=\"fill\" />\n          </span>\n        </div>\n      )}\n\n      {showProgress && !controls && (\n        <div className=\"absolute inset-x-0 bottom-0 px-3 pb-2.5 pt-6 bg-gradient-to-t from-black/40 to-transparent\">\n          <div\n            role=\"slider\"\n            aria-label=\"Playback position\"\n            aria-valuenow={scrubberPercent}\n            aria-valuemin={0}\n            aria-valuemax={100}\n            tabIndex={0}\n            ref={trackRef}\n            className=\"relative flex h-4 w-full cursor-pointer items-center\"\n            onMouseDown={handleTrackPointerDown}\n            onTouchStart={handleTrackPointerDown}\n            onClick={(e) => e.stopPropagation()}\n            onKeyDown={handleScrubberKeyDown}\n          >\n            <div className=\"w-full overflow-hidden rounded-full bg-white/30 h-1\">\n              <div\n                className=\"h-full rounded-full bg-white\"\n                style={{ width: `${scrubberPercent}%` }}\n              />\n            </div>\n          </div>\n        </div>\n      )}\n\n      {controls && (\n        <div className=\"absolute inset-x-0 bottom-0 flex items-center gap-2 bg-gradient-to-t from-black/60 to-transparent px-3 pb-2.5 pt-6 transition-all duration-200\">\n          <button\n            type=\"button\"\n            onClick={(e) => {\n              e.stopPropagation()\n              setPlaying((p) => !p)\n            }}\n            className=\"shrink-0 text-white\"\n            aria-label={playing ? 'Pause' : 'Play'}\n          >\n            {playing ? (\n              <PauseIcon className=\"size-5\" weight=\"fill\" />\n            ) : (\n              <PlayIcon className=\"size-5 translate-x-px\" weight=\"fill\" />\n            )}\n          </button>\n\n          <div\n            role=\"slider\"\n            aria-label=\"Playback position\"\n            aria-valuenow={scrubberPercent}\n            aria-valuemin={0}\n            aria-valuemax={100}\n            tabIndex={0}\n            ref={trackRef}\n            className=\"relative flex h-4 w-full cursor-pointer items-center\"\n            onMouseDown={handleTrackPointerDown}\n            onTouchStart={handleTrackPointerDown}\n            onClick={(e) => e.stopPropagation()}\n            onMouseEnter={() => setScrubberHovered(true)}\n            onMouseLeave={() => setScrubberHovered(false)}\n            onKeyDown={handleScrubberKeyDown}\n          >\n            <div\n              className={`w-full overflow-hidden rounded-full bg-white/30 transition-all duration-200 ${scrubberHovered || seeking ? 'h-1.5' : 'h-1'}`}\n            >\n              <div\n                className=\"h-full rounded-full bg-white\"\n                style={{ width: `${scrubberPercent}%` }}\n              />\n            </div>\n            <div\n              className={`absolute size-3 -translate-x-1/2 rounded-full bg-white shadow transition-[opacity,transform] duration-200 ${scrubberHovered || seeking ? 'scale-100 opacity-100' : 'scale-0 opacity-0'}`}\n              style={{ left: `${scrubberPercent}%` }}\n            />\n          </div>\n        </div>\n      )}\n    </div>\n  )\n}\n\nexport default MediaPlayer\n","import React, { useState } from 'react'\n\nimport { optimizeMessagingAttachmentUrl } from '../../utils/cdnImageUrl'\n\nimport MediaPlayer, { type MediaPlayerProps } from './MediaPlayer'\nimport { renderTypeIcon } from './utils/icons'\nimport { getSourceType } from './utils/mimeType'\n\nexport type AttachmentThumbnailVariant = 'light' | 'dark'\n\nexport interface AttachmentThumbnailProps {\n  mimeType: string\n  sourceUrl?: string\n  thumbnailUrl?: string\n  title?: string\n  variant: AttachmentThumbnailVariant\n  /** Extra props passed to MediaPlayer when source is video or audio. */\n  mediaPlayerProps?: Partial<\n    Pick<\n      MediaPlayerProps,\n      'autoPlay' | 'loop' | 'muted' | 'controls' | 'onContainerClick'\n    >\n  >\n  /**\n   * When true (Visitor unlocked image/document), use aspect-video + object-contain fade-in.\n   */\n  containedImage?: boolean\n}\n\nconst placeholderIconClass = (variant: AttachmentThumbnailVariant) =>\n  variant === 'dark' ? 'size-12 text-white/20' : 'size-12 text-black/20'\n\nconst posterShellClass = (variant: AttachmentThumbnailVariant) =>\n  variant === 'dark'\n    ? 'aspect-video overflow-hidden bg-white/10'\n    : 'aspect-video overflow-hidden bg-black/5'\n\n/**\n * Renders the media preview area for attachment cards (LockedAttachment, MediaMessage).\n * Overlays (dim, lock, eye toggle) are composed by the parent.\n */\nconst AttachmentThumbnail: React.FC<AttachmentThumbnailProps> = ({\n  mimeType,\n  sourceUrl,\n  thumbnailUrl,\n  title,\n  variant,\n  mediaPlayerProps,\n  containedImage = false,\n}) => {\n  const sourceType = getSourceType(mimeType)\n  const [sourceReady, setSourceReady] = useState(false)\n  const optimizedSourceUrl = optimizeMessagingAttachmentUrl(sourceUrl)\n  const optimizedThumbnailUrl = optimizeMessagingAttachmentUrl(thumbnailUrl)\n\n  if (sourceUrl && (sourceType === 'video' || sourceType === 'audio')) {\n    return (\n      <MediaPlayer\n        source={sourceUrl}\n        mimeType={mimeType}\n        poster={optimizedThumbnailUrl}\n        controls\n        {...mediaPlayerProps}\n      />\n    )\n  }\n\n  if (sourceUrl && sourceType === 'image') {\n    if (containedImage) {\n      return (\n        <div className={`relative ${posterShellClass(variant)}`}>\n          <img\n            src={optimizedSourceUrl ?? sourceUrl}\n            alt={title ?? ''}\n            className={`absolute inset-0 h-full w-full object-contain transition-opacity duration-300 ${sourceReady ? 'opacity-100' : 'opacity-0'}`}\n            draggable={false}\n            onLoad={() => setSourceReady(true)}\n          />\n        </div>\n      )\n    }\n    return (\n      <img\n        src={optimizedSourceUrl ?? sourceUrl}\n        alt={title ?? ''}\n        className=\"block w-full\"\n        draggable={false}\n      />\n    )\n  }\n\n  if (sourceUrl && sourceType === 'document') {\n    if (thumbnailUrl) {\n      if (containedImage) {\n        return (\n          <div className={`relative ${posterShellClass(variant)}`}>\n            <img\n              src={optimizedThumbnailUrl ?? thumbnailUrl}\n              alt={title ?? ''}\n              className={`absolute inset-0 h-full w-full object-contain transition-opacity duration-300 ${sourceReady ? 'opacity-100' : 'opacity-0'}`}\n              draggable={false}\n              onLoad={() => setSourceReady(true)}\n            />\n          </div>\n        )\n      }\n      return (\n        <img\n          src={optimizedThumbnailUrl ?? thumbnailUrl}\n          alt=\"\"\n          className=\"block w-full\"\n          draggable={false}\n        />\n      )\n    }\n    return (\n      <div\n        className={`flex aspect-video w-full items-center justify-center ${variant === 'dark' ? 'bg-white/10' : 'bg-black/5'}`}\n      >\n        {renderTypeIcon(mimeType, {\n          className: placeholderIconClass(variant),\n          weight: 'regular',\n        })}\n      </div>\n    )\n  }\n\n  // Poster-only or empty (no sourceUrl)\n  if (thumbnailUrl) {\n    return (\n      <div className={`relative ${posterShellClass(variant)}`}>\n        <img\n          src={optimizedThumbnailUrl ?? thumbnailUrl}\n          alt={title ?? ''}\n          draggable={false}\n          className=\"absolute inset-0 h-full w-full object-cover\"\n        />\n      </div>\n    )\n  }\n\n  return (\n    <div\n      className={`flex aspect-video w-full items-center justify-center ${variant === 'dark' ? 'bg-white/10' : 'bg-black/5'}`}\n    >\n      {renderTypeIcon(mimeType, {\n        className: placeholderIconClass(variant),\n        weight: 'regular',\n      })}\n    </div>\n  )\n}\n\nexport default AttachmentThumbnail\n"],"mappings":"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"}