// frontend\src\components\ui\FinishedVideoPreview.tsx
'use client'

import { useCallback, useEffect, useMemo, useRef, useState, type SyntheticEvent } from 'react'
import type { RecordJob } from '../../types'
import HoverPopover from './HoverPopover'
import { DEFAULT_INLINE_MUTED, applyInlineVideoPolicy } from './videoPolicy'
import { ForwardIcon } from '@heroicons/react/24/solid'
import LoadingSpinner from './LoadingSpinner'

type Variant = 'thumb' | 'fill'
type InlineVideoMode = false | true | 'always' | 'hover'
type AnimatedMode = 'frames' | 'clips' | 'teaser'
type AnimatedTrigger = 'always' | 'hover'
type ProgressKind = 'inline' | 'teaser' | 'clips'

export type FinishedVideoPreviewProps = {
  job: RecordJob
  getFileName: (path: string) => string

  /** optional legacy override (z.B. aus Cache im Parent) */
  durationSeconds?: number

  /** Callbacks für Parent-State */
  onDuration?: (job: RecordJob, seconds: number) => void
  onResolution?: (job: RecordJob, w: number, h: number) => void

  /** animated="true": frames = wechselnde Bilder, clips = 0.75s-Teaser-Clips (z.B. 12), teaser = vorgerendertes MP4 */
  animated?: boolean
  animatedMode?: AnimatedMode
  animatedTrigger?: AnimatedTrigger

  /** nur für frames */
  autoTickMs?: number
  thumbStepSec?: number
  thumbSpread?: boolean
  thumbSamples?: number

  /** nur für clips */
  clipSeconds?: number
  clipCount?: number

  /** neu: thumb = w-20 h-16, fill = w-full h-full */
  variant?: Variant
  className?: string
  showPopover?: boolean

  blur?: boolean

  /**
   * inline video:
   * - false: nur Bild/Teaser
   * - true/'always': immer inline abspielen (wenn inView)
   * - 'hover': nur bei Hover/Focus abspielen, sonst Bild
   */
  inlineVideo?: InlineVideoMode
  /** wenn sich dieser Wert ändert, wird das inline-video neu gemounted -> startet bei 0 */
  inlineNonce?: number
  /** Inline-Playback: Controls anzeigen? */
  inlineControls?: boolean
  /** Inline-Playback: loopen? */
  inlineLoop?: boolean

  assetNonce?: number

  /** alle Inline/Teaser/Clips muted? (Default: true) */
  muted?: boolean
  /** Popover-Video muted? (Default: true) */
  popoverMuted?: boolean

  /** Still-Preview (Bild) unabhängig vom inView-Gating laden */
  alwaysLoadStill?: boolean

  /** Teaser-Datei vorladen, bevor das Element wirklich im Viewport ist */
  teaserPreloadEnabled?: boolean

  /** Vorlade-Zone für Teaser (IntersectionObserver rootMargin) */
  teaserPreloadRootMargin?: string

  scrubProgressRatio?: number
  preferScrubProgress?: boolean

  forceActive?: boolean
  activationNonce?: number
  registerTeaserPlay?: (fn: (() => void) | null) => void
  holdSpeedActive?: boolean
}

function baseName(path: string) {
  return (path || '').split(/[\\/]/).pop() || ''
}

export default function FinishedVideoPreview({
  job,
  getFileName,
  durationSeconds,
  onDuration,
  onResolution,

  animated = false,
  animatedMode = 'frames',
  animatedTrigger = 'always',

  autoTickMs = 15000,
  thumbStepSec,
  thumbSpread,
  thumbSamples,

  clipSeconds = 0.75,
  clipCount = 12,

  variant = 'thumb',
  className,
  showPopover = true,
  blur = false,

  inlineVideo = false,
  inlineNonce = 0,
  inlineControls = false,
  inlineLoop = true,

  assetNonce = 0,

  muted = DEFAULT_INLINE_MUTED,
  popoverMuted = DEFAULT_INLINE_MUTED,
  alwaysLoadStill = false,
  teaserPreloadEnabled = false,
  teaserPreloadRootMargin = '700px 0px',
  scrubProgressRatio,
  preferScrubProgress = false,
  forceActive= false,
  activationNonce = 0,
  registerTeaserPlay,
  holdSpeedActive = false,
}: FinishedVideoPreviewProps) {
  const file = baseName(job.output || '') || getFileName(job.output || '')
  const blurCls = blur ? 'blur-md' : ''

  // ✅ meta robust normalisieren (job.meta kann string sein)
  const meta = useMemo(() => {
    const m: any = (job as any)?.meta
    if (!m) return null
    if (typeof m === 'string') {
      try {
        return JSON.parse(m)
      } catch {
        return null
      }
    }
    return m
  }, [job])

  // ✅ merge statt "meta ?? fetchedMeta"
  // job.meta bleibt Basis, fetchedMeta ergänzt fehlende Felder (z.B. previewClips)
  const metaForPreview = meta
  
  const [progressMountTick, setProgressMountTick] = useState(0)

  // previewClips mapping: preview.mp4 ist Concatenation von Segmenten
  type PreviewClip = { startSeconds: number; durationSeconds: number }
  type PreviewClipMap = { start: number; dur: number; cumStart: number; cumEnd: number }

  const previewClipMap = useMemo<PreviewClipMap[] | null>(() => {
    let pcsAny: any = (metaForPreview as any)?.previewClips

    // ✅ falls previewClips als JSON-string gespeichert ist
    if (typeof pcsAny === 'string') {
      try {
        pcsAny = JSON.parse(pcsAny)
      } catch {
        pcsAny = null
      }
    }

    // ✅ falls es verschachtelt ist (falls du sowas irgendwo hast)
    if (!Array.isArray(pcsAny) && Array.isArray((metaForPreview as any)?.preview?.clips)) {
      pcsAny = (metaForPreview as any).preview.clips
    }

    const pcs = pcsAny as PreviewClip[] | null
    if (!Array.isArray(pcs) || pcs.length === 0) return null

    let cum = 0
    const out: PreviewClipMap[] = []

    for (const c of pcs) {
      const start = Number((c as any)?.startSeconds)
      const dur = Number((c as any)?.durationSeconds)
      if (!Number.isFinite(start) || start < 0) continue
      if (!Number.isFinite(dur) || dur <= 0) continue

      const cumStart = cum
      const cumEnd = cum + dur
      out.push({ start, dur, cumStart, cumEnd })
      cum = cumEnd
    }

    return out.length ? out : null
  }, [metaForPreview])

  const previewClipMapKey = useMemo(() => {
    if (!previewClipMap) return ''
    // stabiler key für effect-deps
    return previewClipMap.map((c) => `${c.start.toFixed(3)}:${c.dur.toFixed(3)}`).join('|')
  }, [previewClipMap])

  // (aktuell ungenutzt, aber bewusst drin gelassen)
  const mapPreviewTimeToGlobalTime = (tPreview: number, totalSeconds?: number) => {
    const m = previewClipMap
    if (!m || !m.length) return tPreview

    // clamp
    if (tPreview <= 0) return m[0].start

    const last = m[m.length - 1]
    if (tPreview >= last.cumEnd) {
      // ✅ Wenn wir die Vollvideo-Dauer kennen: Teaser-Ende = 100% (Vollvideo-Ende)
      if (typeof totalSeconds === 'number' && Number.isFinite(totalSeconds) && totalSeconds > 0) return totalSeconds
      // Fallback
      return last.start + last.dur
    }

    // binary search nach cumStart/cumEnd
    let lo = 0
    let hi = m.length - 1
    while (lo <= hi) {
      const mid = (lo + hi) >> 1
      const c = m[mid]
      if (tPreview < c.cumStart) hi = mid - 1
      else if (tPreview >= c.cumEnd) lo = mid + 1
      else {
        // innerhalb des Segments
        return c.start + (tPreview - c.cumStart)
      }
    }

    // Fallback: nächster Clip
    const idx = Math.max(0, Math.min(m.length - 1, lo))
    const c = m[idx]
    return c.start
  }
  void mapPreviewTimeToGlobalTime

  const effectiveW =
    (typeof meta?.videoWidth === 'number' && Number.isFinite(meta.videoWidth) && meta.videoWidth > 0 ? meta.videoWidth : undefined) ??
    (typeof job.videoWidth === 'number' && Number.isFinite(job.videoWidth) && job.videoWidth > 0 ? job.videoWidth : undefined)

  const effectiveH =
    (typeof meta?.videoHeight === 'number' &&
    Number.isFinite(meta.videoHeight) &&
    meta.videoHeight > 0
      ? meta.videoHeight
      : undefined) ??
    (typeof job.videoHeight === 'number' && Number.isFinite(job.videoHeight) && job.videoHeight > 0 ? job.videoHeight : undefined)

  const effectiveSizeBytes =
    (typeof meta?.fileSize === 'number' && Number.isFinite(meta.fileSize) && meta.fileSize > 0 ? meta.fileSize : undefined) ??
    (typeof job.sizeBytes === 'number' && Number.isFinite(job.sizeBytes) && job.sizeBytes > 0 ? job.sizeBytes : undefined)

  const effectiveFPS =
    (typeof meta?.fps === 'number' && Number.isFinite(meta.fps) && meta.fps > 0 ? meta.fps : undefined) ??
    (typeof job.fps === 'number' && Number.isFinite(job.fps) && job.fps > 0 ? job.fps : undefined)

  // --- Duration normalisieren: manche Quellen liefern ms statt s
  const rawDuration =
    (typeof (metaForPreview as any)?.durationSeconds === 'number' &&
    Number.isFinite((metaForPreview as any).durationSeconds) &&
    (metaForPreview as any).durationSeconds > 0
      ? (metaForPreview as any).durationSeconds
      : undefined) ??
    (typeof job.durationSeconds === 'number' && Number.isFinite(job.durationSeconds) && job.durationSeconds > 0
      ? job.durationSeconds
      : undefined) ??
    (typeof durationSeconds === 'number' && Number.isFinite(durationSeconds) && durationSeconds > 0 ? durationSeconds : undefined)

  // Heuristik: > 24h in Sekunden ist unplausibel für Clips; außerdem sieht ms typischerweise wie 600000 aus.
  const effectiveDurationSec =
    typeof rawDuration === 'number' && Number.isFinite(rawDuration) && rawDuration > 0
      ? rawDuration > 24 * 60 * 60
        ? rawDuration / 1000
        : rawDuration
      : undefined

  const hasDuration = typeof effectiveDurationSec === 'number' && Number.isFinite(effectiveDurationSec) && effectiveDurationSec > 0
  const hasResolution =
    typeof effectiveW === 'number' &&
    typeof effectiveH === 'number' &&
    Number.isFinite(effectiveW) &&
    Number.isFinite(effectiveH) &&
    effectiveW > 0 &&
    effectiveH > 0

  const commonVideoProps = {
    muted,
    playsInline: true,
    preload: 'metadata' as const,
  }

  const [thumbOk, setThumbOk] = useState(true)
  const [videoOk, setVideoOk] = useState(true)
  const [metaLoaded, setMetaLoaded] = useState(false)

  const [teaserReady, setTeaserReady] = useState(false)
  const [teaserOk, setTeaserOk] = useState(true)

  // ✅ verhindert doppelte Parent-Callbacks (z.B. Count/Meta-Refresh im Parent)
  const emittedDurationRef = useRef<string>('')
  const emittedResolutionRef = useRef<string>('')

  // inView (Viewport)
  const rootRef = useRef<HTMLDivElement | null>(null)
  const [inView, setInView] = useState(false)
  const [nearView, setNearView] = useState(false)

  // Tick nur für frames-Mode
  const [localTick, setLocalTick] = useState(0)

  // Hover-State (für inline hover ODER teaser hover)
  const [hovered, setHovered] = useState(false)

  const [pageVisible, setPageVisible] = useState(() => {
    if (typeof document === 'undefined') return true
    return !document.hidden
  })

  const effectiveInView = forceActive || inView
  const effectiveNearView = forceActive || nearView

  const inlineMode: 'never' | 'always' | 'hover' =
    inlineVideo === true || inlineVideo === 'always' ? 'always' : inlineVideo === 'hover' ? 'hover' : 'never'

  // --- Hover-Events brauchen wir, wenn inline hover ODER teaser hover
  const wantsHover =
    inlineMode === 'hover' || (animated && (animatedMode === 'clips' || animatedMode === 'teaser') && animatedTrigger === 'hover')

  const stripHot = (s: string) => (s.startsWith('HOT ') ? s.slice(4) : s)

  const previewId = useMemo(() => {
    const f = baseName(job.output || '') || getFileName(job.output || '')
    if (!f) return ''
    const base = f.replace(/\.[^.]+$/, '') // ext weg
    return stripHot(base).trim()
  }, [job.output, getFileName])

  // Vollvideo (für Inline-Playback + Fallback-Metadaten via loadedmetadata)
  const videoSrc = useMemo(() => (file ? `/api/record/video?file=${encodeURIComponent(file)}` : ''), [file])

  const sizeClass = variant === 'fill' ? 'w-full h-full' : 'w-20 h-16'

  const inlineRef = useRef<HTMLVideoElement | null>(null)
  const teaserMp4Ref = useRef<HTMLVideoElement | null>(null)
  const clipsRef = useRef<HTMLVideoElement | null>(null)
  const teaserSoundForcedRef = useRef(false)
  const teaserMutedRef = useRef(muted)

  const setTeaserPlaybackRate = useCallback((rate: number) => {
    const el = teaserMp4Ref.current
    if (!el) return

    try {
      el.playbackRate = rate
      el.defaultPlaybackRate = rate
    } catch {
      // ignore
    }
  }, [])

  useEffect(() => {
    teaserMutedRef.current = muted

    const el = teaserMp4Ref.current
    if (!el) return

    const effectiveMuted = teaserSoundForcedRef.current ? false : teaserMutedRef.current

    applyInlineVideoPolicy(el, { muted: effectiveMuted })

    try {
      el.muted = effectiveMuted
      el.defaultMuted = effectiveMuted
    } catch {
      // ignore
    }
  }, [muted])
  
  const playTeaserFromGesture = useCallback((withSound = true) => {
    const el = teaserMp4Ref.current
    if (!el) return false

    const nextMuted = !withSound

    applyInlineVideoPolicy(el, { muted: nextMuted })

    try {
      el.pause()
    } catch {}

    try {
      el.muted = nextMuted
      el.defaultMuted = nextMuted
      el.playsInline = true
      el.loop = true
      el.autoplay = true
      el.currentTime = 0
    } catch {}

    const p = el.play?.()
    if (p && typeof (p as any).catch === 'function') {
      ;(p as Promise<void>).catch(() => {})
    }

    return true
  }, [])

  useEffect(() => {
    return () => {
      setTeaserPlaybackRate(1)
    }
  }, [setTeaserPlaybackRate])

  useEffect(() => {
    if (!registerTeaserPlay) return

    registerTeaserPlay(() => {
      teaserSoundForcedRef.current = true
      return playTeaserFromGesture(true)
    })

    return () => registerTeaserPlay(null)
  }, [registerTeaserPlay, playTeaserFromGesture])

  const teaserPlayRetryTimerRef = useRef<number | null>(null)
  const teaserLastProgressTimeRef = useRef<number>(0)

  const lastMountedRef = useRef<{
    inline: HTMLVideoElement | null
    teaser: HTMLVideoElement | null
    clips: HTMLVideoElement | null
  }>({
    inline: null,
    teaser: null,
    clips: null,
  })

  const bumpMountTickIfNew = (slot: 'inline' | 'teaser' | 'clips', el: HTMLVideoElement | null) => {
    if (!el) return
    if (lastMountedRef.current[slot] === el) return // ✅ gleicher DOM-Node -> NICHT nochmal tickern
    lastMountedRef.current[slot] = el // ✅ erst merken, dann state setzen
    setProgressMountTick((x) => x + 1)
  }

  // ▶️ Progressbar für abgespieltes Preview/Inline-Video
  const [playRatio, setPlayRatio] = useState(0)
  const [, setPlayGlobalSec] = useState(0)

  const clamp01 = (x: number) => (x < 0 ? 0 : x > 1 ? 1 : x)

  // ✅ FIX: Teaser-Mapping darf NICHT von currentSrc abhängen.
  // Ratio basiert auf vvDur (z.B. 2/18) — unabhängig von totalSeconds.
  const readProgressStepped = (
    vv: HTMLVideoElement | null,
    totalSeconds: number | undefined,
    stepSec = clipSeconds,
    forceTeaserMap = false
  ): { ratio: number; globalSec: number; vvDur: number } => {
    if (!vv) return { ratio: 0, globalSec: 0, vvDur: 0 }

    const vvDur = Number(vv.duration)
    const vvDurOk = Number.isFinite(vvDur) && vvDur > 0
    if (!vvDurOk) return { ratio: 0, globalSec: 0, vvDur: 0 }

    const tPreview = Number(vv.currentTime)
    if (!Number.isFinite(tPreview) || tPreview < 0) return { ratio: 0, globalSec: 0, vvDur }

    const m = previewClipMap
    let globalSec = 0

    if (forceTeaserMap && Array.isArray(m) && m.length > 0) {
      const last = m[m.length - 1]

      if (tPreview >= last.cumEnd) {
        globalSec =
          typeof totalSeconds === 'number' && Number.isFinite(totalSeconds) && totalSeconds > 0
            ? totalSeconds
            : last.start + last.dur
      } else {
        let lo = 0
        let hi = m.length - 1
        let segIdx = 0

        while (lo <= hi) {
          const mid = (lo + hi) >> 1
          const c = m[mid]
          if (tPreview < c.cumStart) hi = mid - 1
          else if (tPreview >= c.cumEnd) lo = mid + 1
          else {
            segIdx = mid
            break
          }
        }

        const seg = m[segIdx]
        globalSec = seg.start

        // ✅ Sichtbarer Teaser-Progress exakt am Segmentindex ausrichten
        // springt auf Beginn des aktuell aktiven Preview-Segments (kein floor-lag über currentTime)
        const ratio = m.length > 0 ? clamp01(segIdx / m.length) : 0

        return { ratio, globalSec: Math.max(0, globalSec), vvDur }
      }
    }

    // inline/clips fallback
    if (Number.isFinite(stepSec) && stepSec > 0) {
      globalSec = Math.floor(tPreview / stepSec) * stepSec
    } else {
      globalSec = tPreview
    }

    const ratio = clamp01(Math.min(globalSec, vvDur) / vvDur)
    return { ratio, globalSec: Math.max(0, globalSec), vvDur }
  }

  const hardStop = (v: HTMLVideoElement | null) => {
    if (!v) return
    try {
      v.pause()
    } catch {}
    try {
      v.removeAttribute('src')
      // @ts-ignore
      v.src = ''
      v.load()
    } catch {}
  }

  useEffect(() => {
    return () => {
      if (teaserPlayRetryTimerRef.current != null) {
        window.clearTimeout(teaserPlayRetryTimerRef.current)
        teaserPlayRetryTimerRef.current = null
      }
    }
  }, [])

  useEffect(() => {
    if (typeof document === 'undefined') return

    const onVisibilityChange = () => {
      setPageVisible(!document.hidden)
    }

    document.addEventListener('visibilitychange', onVisibilityChange)
    window.addEventListener('pageshow', onVisibilityChange)
    window.addEventListener('focus', onVisibilityChange)

    return () => {
      document.removeEventListener('visibilitychange', onVisibilityChange)
      window.removeEventListener('pageshow', onVisibilityChange)
      window.removeEventListener('focus', onVisibilityChange)
    }
  }, [])

  useEffect(() => {
    setTeaserReady(false)
    setTeaserOk(true)

    // ✅ neue Datei/Asset -> Callback-Dedupe zurücksetzen
    emittedDurationRef.current = ''
    emittedResolutionRef.current = ''
    teaserSoundForcedRef.current = false
    setMetaLoaded(false)
  }, [previewId, assetNonce])

  useEffect(() => {
    const onRelease = (ev: any) => {
      const f = String(ev?.detail?.file ?? '')
      if (!f || f !== file) return
      hardStop(inlineRef.current)
      hardStop(teaserMp4Ref.current)
      hardStop(clipsRef.current)
    }
    window.addEventListener('player:release', onRelease as EventListener)
    window.addEventListener('player:close', onRelease as EventListener)
    return () => {
      window.removeEventListener('player:release', onRelease as EventListener)
      window.removeEventListener('player:close', onRelease as EventListener)
    }
  }, [file])

  // --- IntersectionObserver: echtes inView (für Playback)
  useEffect(() => {
    const el = rootRef.current
    if (!el) return

    const obs = new IntersectionObserver(
      (entries) => {
        const hit = Boolean(entries[0]?.isIntersecting)
        setInView(hit)
      },
      {
        threshold: 0.01,
        rootMargin: '0px',
      }
    )

    obs.observe(el)
    return () => obs.disconnect()
  }, [])

  // --- IntersectionObserver: nearView (für Teaser-Preload / Vorwärmen)
  useEffect(() => {
    const el = rootRef.current
    if (!el) return

    // Falls Preload deaktiviert ist: nearView folgt inView
    if (!teaserPreloadEnabled) {
      setNearView(inView)
      return
    }

    let armed = true
    const obs = new IntersectionObserver(
      (entries) => {
        const hit = Boolean(entries[0]?.isIntersecting)
        if (hit) {
          setNearView(true)
          // einmal "armed" reicht (wir wollen nicht wieder entladen)
          if (armed) {
            armed = false
            obs.disconnect()
          }
        }
      },
      {
        threshold: 0,
        rootMargin: teaserPreloadRootMargin,
      }
    )

    obs.observe(el)
    return () => obs.disconnect()
  }, [teaserPreloadEnabled, teaserPreloadRootMargin, inView])

  // --- Tick für "frames"
    useEffect(() => {
    if (!animated) return
    if (animatedMode !== 'frames') return
    if (!effectiveInView || !pageVisible) return

    const id = window.setInterval(() => setLocalTick((t) => t + 1), autoTickMs)
    return () => window.clearInterval(id)
  }, [animated, animatedMode, effectiveInView, pageVisible, autoTickMs])

  // --- Thumbnail time (nur frames!)
  const thumbTimeSec = useMemo(() => {
    if (!animated) return null
    if (animatedMode !== 'frames') return null
    if (!hasDuration) return null

    const dur = effectiveDurationSec!
    const step = Math.max(0.25, thumbStepSec ?? 3)

    if (thumbSpread) {
      const count = Math.max(4, Math.min(thumbSamples ?? 16, Math.floor(dur)))
      const idx = localTick % count
      const span = Math.max(0.1, dur - step)
      const base = Math.min(0.25, span * 0.02)
      const t = (idx / count) * span + base
      return Math.min(dur - 0.05, Math.max(0.05, t))
    }

    const total = Math.max(dur - 0.1, step)
    const t = (localTick * step) % total
    return Math.min(dur - 0.05, Math.max(0.05, t))
  }, [animated, animatedMode, hasDuration, effectiveDurationSec, localTick, thumbStepSec, thumbSpread, thumbSamples])

  const v = assetNonce ?? 0

  const thumbSrc = useMemo(() => {
    if (!previewId) return ''
    if (thumbTimeSec == null) return `/api/preview?id=${encodeURIComponent(previewId)}&v=${v}`
    return `/api/preview?id=${encodeURIComponent(previewId)}&t=${thumbTimeSec}&v=${v}`
  }, [previewId, thumbTimeSec, v])

  const teaserSrc = useMemo(() => {
    if (!previewId) return ''
    return `/api/generated/teaser?id=${encodeURIComponent(previewId)}&noGenerate=1`
  }, [previewId])

  // --- Inline Video sichtbar?
  const showingInlineVideo =
    inlineMode !== 'never' &&
    effectiveInView &&
    videoOk &&
    (inlineMode === 'always' || (inlineMode === 'hover' && hovered))

  // --- Teaser/Clips aktiv? (sichtbar/erzwungen aktiv, nicht inline, optional nur hover)
  const teaserActive =
    animated &&
    effectiveInView &&
    videoOk &&
    !showingInlineVideo &&
    (animatedTrigger === 'always' || hovered) &&
    (
      (animatedMode === 'teaser' && teaserOk && Boolean(teaserSrc)) ||
      (animatedMode === 'clips' && hasDuration)
    )

  const canHoldSpeedTeaser =
    teaserActive &&
    animatedMode === 'teaser' &&
    !showingInlineVideo

  const teaserSpeedHold = holdSpeedActive && canHoldSpeedTeaser

  const progressTotalSeconds =
    hasDuration && typeof effectiveDurationSec === 'number' ? effectiveDurationSec : undefined

  // ✅ Still-Bild: optional immer laden (entkoppelt vom inView-Gating)
  const shouldLoadStill =
    forceActive || alwaysLoadStill || effectiveInView || (wantsHover && hovered)

  // ✅ Teaser/Clips "vorwärmen": schon in nearView erlauben
  const shouldPreloadAnimatedAssets =
    forceActive || effectiveNearView || effectiveInView || (wantsHover && hovered)

  useEffect(() => {
    if (animatedMode !== 'teaser' || showingInlineVideo || !teaserActive) {
      setTeaserPlaybackRate(1)
      return
    }

    setTeaserPlaybackRate(teaserSpeedHold ? 2 : 1)
  }, [
    teaserSpeedHold,
    teaserActive,
    animatedMode,
    showingInlineVideo,
    setTeaserPlaybackRate,
  ])

  // ✅ Wenn meta.json schon alles hat: sofort Callbacks auslösen (kein hidden <video> nötig)
  //    aber pro Datei/Wert nur 1x (verhindert doppelte Parent-Requests)
  useEffect(() => {
    let did = false

    if (onDuration && hasDuration) {
      const secs = Number(effectiveDurationSec)
      const durationKey = `${file}|dur|${secs}`

      if (emittedDurationRef.current !== durationKey) {
        emittedDurationRef.current = durationKey
        onDuration(job, secs)
        did = true
      }
    }

    if (onResolution && hasResolution) {
      const w = Number(effectiveW)
      const h = Number(effectiveH)
      const resKey = `${file}|res|${w}x${h}`

      if (emittedResolutionRef.current !== resKey) {
        emittedResolutionRef.current = resKey
        onResolution(job, w, h)
        did = true
      }
    }

    if (did) setMetaLoaded(true)
  }, [file, job, onDuration, onResolution, hasDuration, hasResolution, effectiveDurationSec, effectiveW, effectiveH])

  // ✅ Fallback: nur wenn wir wirklich noch nichts haben, über loadedmetadata nachladen
  const handleLoadedMetadata = (e: SyntheticEvent<HTMLVideoElement>) => {
    setMetaLoaded(true)

    const vv = e.currentTarget

    if (onDuration && !hasDuration) {
      const secs = Number(vv.duration)
      if (Number.isFinite(secs) && secs > 0) {
        const durationKey = `${file}|dur|${secs}`
        if (emittedDurationRef.current !== durationKey) {
          emittedDurationRef.current = durationKey
          onDuration(job, secs)
        }
      }
    }

    if (onResolution && !hasResolution) {
      const w = Number(vv.videoWidth)
      const h = Number(vv.videoHeight)
      if (Number.isFinite(w) && Number.isFinite(h) && w > 0 && h > 0) {
        const resKey = `${file}|res|${w}x${h}`
        if (emittedResolutionRef.current !== resKey) {
          emittedResolutionRef.current = resKey
          onResolution(job, w, h)
        }
      }
    }
  }

  useEffect(() => {
    setThumbOk(true)
    setVideoOk(true)

    // ✅ Mount-Guards zurücksetzen
    lastMountedRef.current.inline = null
    lastMountedRef.current.teaser = null
    lastMountedRef.current.clips = null
  }, [previewId, assetNonce])

  if (!videoSrc) {
    return <div className={[sizeClass, 'rounded bg-gray-100 dark:bg-white/5'].join(' ')} />
  }

  // ✅ aktive Asset-Nutzung (z.B. poster etc.)
  const shouldLoadAssets = shouldPreloadAnimatedAssets
  
  const teaserCanPrewarm =
    animated &&
    animatedMode === 'teaser' &&
    Boolean(teaserSrc) &&
    !showingInlineVideo &&
    shouldPreloadAnimatedAssets &&
    animatedTrigger !== 'hover'
  
  const showStillImage =
    shouldLoadStill &&
    !showingInlineVideo &&
    !(
      animatedMode === 'teaser' &&
      teaserActive &&
      teaserReady
    )

  const teaserActivatedSoftly =
    animatedTrigger === 'hover' && hovered

  const showTeaserLoadingOverlay =
    !showingInlineVideo &&
    animatedMode === 'teaser' &&
    teaserActive &&
    teaserOk &&
    !teaserReady &&
    !teaserActivatedSoftly

  // ✅ Progress-Quelle: NUR das Element, das wirklich spielt (für "Sprünge" wichtig)
  const progressVideoRef =
    showingInlineVideo
      ? inlineRef
      : !showingInlineVideo && teaserActive && animatedMode === 'teaser'
        ? teaserMp4Ref
        : !showingInlineVideo && teaserActive && animatedMode === 'clips'
          ? clipsRef
          : null

    const showProgressBar =
      Boolean(progressVideoRef) &&
      effectiveInView

  const progressKind: ProgressKind =
    showingInlineVideo ? 'inline' : teaserActive && animatedMode === 'teaser' ? 'teaser' : 'clips'

  // ✅ Frames-Progress: zeigt Position des aktuellen Thumbnails relativ zur Gesamtdauer
  const showFrameProgress =
    animated &&
    animatedMode === 'frames' &&
    hasDuration &&
    typeof thumbTimeSec === 'number' &&
    Number.isFinite(thumbTimeSec) &&
    thumbTimeSec >= 0

  const frameRatio = showFrameProgress ? clamp01(thumbTimeSec! / effectiveDurationSec!) : 0

  const hasScrubProgress =
    !showingInlineVideo &&
    preferScrubProgress &&
    typeof scrubProgressRatio === 'number' &&
    Number.isFinite(scrubProgressRatio)

  // finaler Balken:
  // 1) externer Scrub-Progress (wenn aktiv)
  // 2) Video-Progress
  // 3) Frames-Progress
  const progressRatio = hasScrubProgress
    ? clamp01(scrubProgressRatio!)
    : showProgressBar
      ? playRatio
      : showFrameProgress
        ? frameRatio
        : 0

  const hasBasePreviewProgress = showProgressBar || showFrameProgress

  const showAnyProgress =
    !showingInlineVideo && (hasScrubProgress || hasBasePreviewProgress)

  const clipOverlay = useMemo(() => {
    if (!hasDuration) return null
    const total = effectiveDurationSec!
    if (!(total > 0)) return null

    const pcsAny: any = (metaForPreview as any)?.previewClips
    let pcs: any = pcsAny
    if (typeof pcs === 'string') {
      try {
        pcs = JSON.parse(pcs)
      } catch {
        pcs = null
      }
    }
    if (!Array.isArray(pcs) || pcs.length === 0) return null

    const clips = pcs
      .map((c: any) => ({
        start: Number(c?.startSeconds),
        dur: Number(c?.durationSeconds),
      }))
      .filter((c: any) => Number.isFinite(c.start) && c.start >= 0 && Number.isFinite(c.dur) && c.dur > 0)

    if (!clips.length) return null

    return clips.map((c: any) => {
      const left = clamp01(c.start / total)
      const width = clamp01(c.dur / total)
      return { left, width, start: c.start, dur: c.dur }
    })
  }, [metaForPreview, hasDuration, effectiveDurationSec])

  // --- Legacy "clips" Logik
  const clipTimes = useMemo(() => {
    if (!animated) return []
    if (animatedMode !== 'clips') return []
    if (!hasDuration) return []

    const dur = effectiveDurationSec!
    const clipLen = Math.max(0.25, clipSeconds)
    const count = Math.max(8, Math.min(clipCount ?? thumbSamples ?? 12, Math.floor(dur)))

    const span = Math.max(0.1, dur - clipLen)
    const base = Math.min(0.25, span * 0.02)

    const times: number[] = []
    for (let i = 0; i < count; i++) {
      const t = (i / count) * span + base
      times.push(Math.min(dur - 0.05, Math.max(0.05, t)))
    }
    return times
  }, [animated, animatedMode, hasDuration, effectiveDurationSec, thumbSamples, clipSeconds, clipCount])

  const clipTimesKey = useMemo(() => clipTimes.map((t) => t.toFixed(2)).join(','), [clipTimes])

  const clipIdxRef = useRef(0)
  const clipStartRef = useRef(0)

  useEffect(() => {
    const vv = teaserMp4Ref.current
    if (!vv) return

    const active = teaserActive && animatedMode === 'teaser'

    const clearRetryTimer = () => {
      if (teaserPlayRetryTimerRef.current != null) {
        window.clearTimeout(teaserPlayRetryTimerRef.current)
        teaserPlayRetryTimerRef.current = null
      }
    }

    if (!active) {
      setTeaserPlaybackRate(1)
      clearRetryTimer()
      try {
        vv.pause()
      } catch {}
      return
    }

    // Tab hidden => nur pausieren, aber NICHT unmounten
    if (!pageVisible) {
      setTeaserPlaybackRate(1)
      clearRetryTimer()
      try {
        vv.pause()
      } catch {}
      return
    }

    let cancelled = false

    const tryPlay = (opts?: { resetToStart?: boolean }) => {
      if (cancelled) return

      const el = teaserMp4Ref.current
      if (!el) {
        return
      }

      const effectiveMuted = teaserSoundForcedRef.current ? false : teaserMutedRef.current

      applyInlineVideoPolicy(el, { muted: effectiveMuted })

      try {
        el.muted = effectiveMuted
        el.defaultMuted = effectiveMuted
        el.playsInline = true
        el.loop = true
        el.autoplay = true
        el.playbackRate = teaserSpeedHold ? 2 : 1
        el.defaultPlaybackRate = teaserSpeedHold ? 2 : 1
      } catch {}

      if (opts?.resetToStart) {
        try {
          el.currentTime = 0
        } catch {}
      }

      const p = el.play?.()
      if (p && typeof (p as any).catch === 'function') {
        ;(p as Promise<void>).catch(() => {})
      }
    }

    const scheduleRetry = (delayMs: number, resetToStart = false) => {
      clearRetryTimer()
      teaserPlayRetryTimerRef.current = window.setTimeout(() => {
        teaserPlayRetryTimerRef.current = null
        tryPlay({ resetToStart })
      }, delayMs)
    }

    const onPlaying = () => {
      teaserLastProgressTimeRef.current = performance.now()
      setTeaserReady(true)
      clearRetryTimer()
    }

    const onTimeUpdate = () => {
      teaserLastProgressTimeRef.current = performance.now()
      setTeaserReady(true)
    }

    const onLoadedData = () => {
      setTeaserReady(true)
      tryPlay()
      scheduleRetry(120)
    }

    const onCanPlay = () => {
      tryPlay()
      scheduleRetry(120)
    }

    const onPause = () => {
      if (cancelled) return
      const el = teaserMp4Ref.current
      if (!el) return
      if (!active) return
      if (el.ended) return

      // iOS-Fall: erster Frame sichtbar, aber Playback stoppt direkt wieder
      scheduleRetry(120)
    }

    const onWaiting = () => {
      if (cancelled) return
      scheduleRetry(120)
    }

    vv.addEventListener('playing', onPlaying)
    vv.addEventListener('timeupdate', onTimeUpdate)
    vv.addEventListener('loadeddata', onLoadedData)
    vv.addEventListener('canplay', onCanPlay)
    vv.addEventListener('pause', onPause)
    vv.addEventListener('waiting', onWaiting)

    // frischer Start für neue TopCard
    tryPlay({ resetToStart: true })

    const raf1 = requestAnimationFrame(() => {
      tryPlay()
      const raf2 = requestAnimationFrame(() => {
        tryPlay()
      })
      ;(tryPlay as any).__raf2 = raf2
    })

    // Watchdog: falls currentTime nicht vorläuft, nochmal anstoßen
    const watchdog = window.setInterval(() => {
      if (cancelled) return
      const el = teaserMp4Ref.current
      if (!el) return
      if (!active) return

      const now = performance.now()
      const stalledFor = now - teaserLastProgressTimeRef.current
      const ct = Number(el.currentTime)

      if ((!Number.isFinite(ct) || ct <= 0.05) && stalledFor > 180) {
        tryPlay({ resetToStart: true })
        return
      }

      if (el.paused && !el.ended && stalledFor > 180) {
        tryPlay()
      }
    }, 180)

    return () => {
      cancelled = true
      clearRetryTimer()
      vv.removeEventListener('playing', onPlaying)
      vv.removeEventListener('timeupdate', onTimeUpdate)
      vv.removeEventListener('loadeddata', onLoadedData)
      vv.removeEventListener('canplay', onCanPlay)
      vv.removeEventListener('pause', onPause)
      vv.removeEventListener('waiting', onWaiting)
      cancelAnimationFrame(raf1)
      const raf2 = (tryPlay as any).__raf2
      if (typeof raf2 === 'number') cancelAnimationFrame(raf2)
      window.clearInterval(watchdog)
    }
  }, [teaserActive, animatedMode, teaserSrc, pageVisible])

  useEffect(() => {
    if (!activationNonce) return
    if (!teaserActive) return
    if (animatedMode !== 'teaser') return
    if (showingInlineVideo) return

    const vv = teaserMp4Ref.current
    if (!vv) return

    let cancelled = false
    let retryTimer: number | null = null

    const tryRestart = (resetToStart = false) => {
      if (cancelled) return
      const el = teaserMp4Ref.current
      if (!el) {
        return
      }

      const effectiveMuted = teaserSoundForcedRef.current ? false : muted

      applyInlineVideoPolicy(el, { muted: effectiveMuted })

      try {
        el.muted = effectiveMuted
        el.defaultMuted = effectiveMuted
        el.playsInline = true
        el.loop = true
        el.autoplay = true
      } catch {}

      if (resetToStart) {
        try {
          el.pause()
        } catch {}
        try {
          el.currentTime = 0
        } catch {}
      }

      const p = el.play?.()
      if (p && typeof (p as any).catch === 'function') {
        ;(p as Promise<void>).catch(() => {})
      }
    }

    retryTimer = window.setTimeout(() => {
      tryRestart(true)
      requestAnimationFrame(() => {
        tryRestart()
        requestAnimationFrame(() => {
          tryRestart()
        })
      })
    }, 40)

    return () => {
      cancelled = true
      if (retryTimer != null) {
        window.clearTimeout(retryTimer)
      }
    }
  }, [activationNonce, teaserActive, animatedMode, showingInlineVideo])

  // ▶️ Progressbar: global relativ zur Vollvideo-Dauer (mit mapping => Sprünge)
  useEffect(() => {
    if (!showProgressBar) {
      setPlayRatio(0)
      setPlayGlobalSec(0)
      return
    }

    const vv = progressVideoRef?.current ?? null
    if (!vv) {
      setPlayRatio(0)
      setPlayGlobalSec(0)
      return
    }

    let stopped = false
    let timer: number | null = null

    const sync = () => {
      if (stopped) return
      if (!vv.isConnected) return

      const forceMap = progressKind === 'teaser' && Array.isArray(previewClipMap) && previewClipMap.length > 0
      const p = readProgressStepped(vv, progressTotalSeconds, clipSeconds, forceMap)

      setPlayRatio(p.ratio)
      setPlayGlobalSec(p.globalSec)
    }

    // initial sofort
    sync()

    // ✅ Sekundentakt (robust, unabhängig von raf/play-events)
    timer = window.setInterval(sync, 100)

    // optional: bei metadata/timeupdate sofort einmal syncen
    const onLoaded = () => sync()
    const onTime = () => sync()
    vv.addEventListener('loadedmetadata', onLoaded)
    vv.addEventListener('durationchange', onLoaded)
    vv.addEventListener('timeupdate', onTime)

    return () => {
      stopped = true
      if (timer != null) window.clearInterval(timer)

      vv.removeEventListener('loadedmetadata', onLoaded)
      vv.removeEventListener('durationchange', onLoaded)
      vv.removeEventListener('timeupdate', onTime)
    }
  }, [showProgressBar, progressVideoRef, progressTotalSeconds, previewClipMapKey, progressKind, previewClipMap, progressMountTick])

  useEffect(() => {
    if (!showingInlineVideo) return
    applyInlineVideoPolicy(inlineRef.current, { muted })
  }, [showingInlineVideo, muted])

  // Legacy: "clips" spielt 0.75s Segmente aus dem Vollvideo per seek
  useEffect(() => {
    const vv = clipsRef.current
    if (!vv) return

    if (!(teaserActive && animatedMode === 'clips')) {
      if (!teaserActive) vv.pause()
      return
    }

    if (!clipTimes.length) return

    clipIdxRef.current = clipIdxRef.current % clipTimes.length
    clipStartRef.current = clipTimes[clipIdxRef.current]

    const start = () => {
      try {
        vv.currentTime = clipStartRef.current
      } catch {}
      const p = vv.play()
      if (p && typeof (p as any).catch === 'function') (p as Promise<void>).catch(() => {})
    }

    const onLoaded = () => start()

    const onTimeUpdate = () => {
      if (!clipTimes.length) return
      if (vv.currentTime - clipStartRef.current >= clipSeconds) {
        clipIdxRef.current = (clipIdxRef.current + 1) % clipTimes.length
        clipStartRef.current = clipTimes[clipIdxRef.current]
        try {
          vv.currentTime = clipStartRef.current + 0.01
        } catch {}
      }
    }

    vv.addEventListener('loadedmetadata', onLoaded)
    vv.addEventListener('timeupdate', onTimeUpdate)

    if (vv.readyState >= 1) start()

    return () => {
      vv.removeEventListener('loadedmetadata', onLoaded)
      vv.removeEventListener('timeupdate', onTimeUpdate)
      vv.pause()
    }
  }, [teaserActive, animatedMode, clipTimesKey, clipSeconds, clipTimes])

  // ✅ brauchen wir noch hidden-metadata-load?
  const needHiddenMeta =
    (forceActive || effectiveNearView || effectiveInView) &&
    (onDuration || onResolution) &&
    !metaLoaded &&
    !showingInlineVideo &&
    ((onDuration && !hasDuration) || (onResolution && !hasResolution))
    
  const showTeaserSegments =
    Boolean(clipOverlay) &&
    (
      progressKind === 'teaser' ||
      (!showingInlineVideo && hasScrubProgress && animatedMode === 'teaser')
    )

  const previewNode = (
    <div
      ref={rootRef}
      className={[
        'group bg-gray-100 dark:bg-white/5 overflow-hidden relative',
        'select-none touch-manipulation',
        sizeClass,
        className ?? '',
      ].join(' ')}
      style={{
        WebkitTouchCallout: 'none',
        WebkitUserSelect: 'none',
        userSelect: 'none',
      }}
      onContextMenu={(e) => e.preventDefault()}
      onPointerLeave={() => {
        if (wantsHover) setHovered(false)
      }}
      onMouseEnter={wantsHover ? () => setHovered(true) : undefined}
      onFocus={wantsHover ? () => setHovered(true) : undefined}
      onBlur={wantsHover ? () => setHovered(false) : undefined}
      data-duration={hasDuration ? String(effectiveDurationSec) : undefined}
      data-res={hasResolution ? `${effectiveW}x${effectiveH}` : undefined}
      data-size={typeof effectiveSizeBytes === 'number' ? String(effectiveSizeBytes) : undefined}
      data-fps={typeof effectiveFPS === 'number' ? String(effectiveFPS) : undefined}
    >
      {/* ✅ Thumb IMMER als Fallback/Background */}
      {showStillImage && thumbSrc && thumbOk ? (
        <img
          src={thumbSrc}
          loading={alwaysLoadStill ? 'eager' : 'lazy'}
          decoding="async"
          alt={file}
          draggable={false}
          className={[
            'absolute inset-0 w-full h-full object-cover pointer-events-none',
            blurCls,
          ].filter(Boolean).join(' ')}
          style={{
            WebkitTouchCallout: 'none',
            WebkitUserSelect: 'none',
            userSelect: 'none',
          }}
          onError={() => setThumbOk(false)}
        />
      ) : (
        <div className="absolute inset-0 bg-black/10 dark:bg-white/10" />
      )}

      {showTeaserLoadingOverlay ? (
        <div className="absolute inset-0 z-20 grid place-items-center pointer-events-none">
          <div
            className="
              flex items-center gap-2 rounded-lg
              bg-black/45 px-3 py-2
              text-white backdrop-blur-sm
              ring-1 ring-white/10
            "
          >
            <LoadingSpinner size="sm" className="shrink-0" srLabel="Teaser lädt…" />
            <span className="text-xs font-medium">Lade Teaser…</span>
          </div>
        </div>
      ) : null}

      {/* ✅ Inline Full Video (nur wenn sichtbar/aktiv) */}
      {showingInlineVideo ? (
        <video
          {...commonVideoProps}
          ref={(el) => {
            inlineRef.current = el
            bumpMountTickIfNew('inline', el)
          }}
          key={`inline-${previewId}-${inlineNonce}`}
          src={videoSrc}
          className={[
            'absolute inset-0 w-full h-full object-cover',
            blurCls,
            inlineControls ? 'pointer-events-auto' : 'pointer-events-none',
          ]
            .filter(Boolean)
            .join(' ')}
          autoPlay
          muted={muted}
          controls={inlineControls}
          loop={inlineLoop}
          poster={shouldLoadAssets ? (thumbSrc || undefined) : undefined}
          onLoadedMetadata={handleLoadedMetadata}
          onError={() => setVideoOk(false)}
        />
      ) : null}

      {/* ✅ Teaser prewarm (lädt Datei/Metadata vor, bleibt unsichtbar) */}
      {!showingInlineVideo && teaserCanPrewarm && !(teaserActive && animatedMode === 'teaser') ? (
        <video
          key={`teaser-prewarm-${previewId}`}
          src={teaserSrc}
          className="hidden"
          muted
          playsInline
          preload="metadata"
          onLoadedData={() => {
            setTeaserOk(true)
            setTeaserReady(true)
          }}
          onCanPlay={() => {
            setTeaserOk(true)
            setTeaserReady(true)
          }}
          onError={() => {
            setTeaserOk(false)
            setTeaserReady(false)
          }}
        />
      ) : null}

      {/* ✅ Teaser MP4 */}
      {!showingInlineVideo && teaserActive && animatedMode === 'teaser' ? (
        <video
          ref={(el) => {
            teaserMp4Ref.current = el
            bumpMountTickIfNew('teaser', el)
          }}
          src={teaserSrc}
          className={[
            'absolute inset-0 w-full h-full object-cover pointer-events-none',
            blurCls,
            teaserReady ? 'opacity-100' : 'opacity-0',
            'transition-opacity duration-150',
          ]
            .filter(Boolean)
            .join(' ')}
          muted={teaserSoundForcedRef.current ? false : muted}
          playsInline
          autoPlay
          loop
          preload="auto"
          poster={shouldLoadAssets ? (thumbSrc || undefined) : undefined}
          onLoadedData={() => {
            setTeaserOk(true)
            setTeaserReady(true)
          }}
          onCanPlay={() => {
            setTeaserOk(true)
            setTeaserReady(true)
          }}
          onError={() => {
            setTeaserOk(false)
            setTeaserReady(false)
          }}
        />
      ) : null}

      {/* ✅ Legacy clips */}
      {!showingInlineVideo && teaserActive && animatedMode === 'clips' ? (
        <video
          ref={(el) => {
            clipsRef.current = el
            bumpMountTickIfNew('clips', el)
          }}
          key={`clips-${previewId}-${clipTimesKey}`}
          src={videoSrc}
          className={['absolute inset-0 w-full h-full object-cover pointer-events-none', blurCls].filter(Boolean).join(' ')}
          muted={muted}
          playsInline
          preload="metadata"
          poster={shouldLoadAssets ? (thumbSrc || undefined) : undefined}
          onError={() => setVideoOk(false)}
        />
      ) : null}

      {teaserSpeedHold && canHoldSpeedTeaser ? (
        <div className="pointer-events-none absolute inset-x-0 bottom-3 z-40 flex justify-center">
          <div
            className="
              inline-flex items-center gap-1.5
              rounded-full bg-black/70 px-3 py-1
              text-[11px] font-semibold text-white
              shadow-lg ring-1 ring-white/10 backdrop-blur-sm
            "
          >
            <span>2x speed</span>
            <ForwardIcon className="size-3.5 shrink-0" />
          </div>
        </div>
      ) : null}

      {/* ▶️ Progressbar: kräftiger + mehr Kontrast */}
      {showAnyProgress ? (
        <div
          aria-hidden="true"
          className={[
            'absolute left-0 right-0 bottom-0 z-40 pointer-events-none',
            // etwas höher + bei hover deutlich
            'h-0.5 group-hover:h-1',
            'transition-[height] duration-150 ease-out',
            // Track: heller + border/inset für Kontrast
            'rounded-none group-hover:rounded-full',
            'bg-black/35 dark:bg-white/10',
            // darf beim Hover raus “glowen”
            'overflow-hidden group-hover:overflow-visible',
          ].join(' ')}
        >
          {/* 1) Segmente (previewClips) als Markierungen */}
          {showTeaserSegments ? (
            <div className="absolute inset-0">
              {clipOverlay!.map((c, i) => (
                <div
                  key={`seg-${i}-${c.left.toFixed(6)}-${c.width.toFixed(6)}`}
                  className="absolute top-0 bottom-0 bg-white/15 dark:bg-white/20"
                  style={{
                    left: `${c.left * 100}%`,
                    width: `${c.width * 100}%`,
                  }}
                />
              ))}
            </div>
          ) : null}

          {/* 2) Kontinuierlicher Fortschritt (SOLID, kein Gradient) */}
          <div
            className={[
              'absolute inset-0 origin-left',
              progressKind === 'teaser' ? '' : 'transition-transform duration-150 ease-out',
            ].join(' ')}
            style={{
              transform: `scaleX(${clamp01(progressRatio)})`,
              background: 'rgba(99,102,241,0.95)', // indigo-500-ish, kräftig
            }}
          />
        </div>
      ) : null}

      {/* ✅ Metadaten-Fallback nur wenn nötig (und nicht inline) */}
      {needHiddenMeta ? (
        <video src={videoSrc} preload="metadata" muted={muted} playsInline className="hidden" onLoadedMetadata={handleLoadedMetadata} />
      ) : null}
    </div>
  )

  if (!showPopover) return previewNode

  return (
    <HoverPopover
      content={(open) =>
        open && (
          <div className="w-[420px]">
            <div className="aspect-video">
              <video
                src={videoSrc}
                className={['w-full h-full bg-black', blurCls].filter(Boolean).join(' ')}
                muted={popoverMuted}
                playsInline
                preload="metadata"
                controls
                autoPlay
                loop
                onClick={(e) => e.stopPropagation()}
                onMouseDown={(e) => e.stopPropagation()}
              />
            </div>
          </div>
        )
      }
    >
      {previewNode}
    </HoverPopover>
  )
}
