> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bfl.ml/llms.txt
> Use this file to discover all available pages before exploring further.

# Video

> Generate, animate, and continue video with FLUX 3. Edit existing footage with the separate FLUX Video Edit [fast] tool.

export const CameraTermNav = ({sections, label} = {}) => {
  const SECTIONS = sections || [{
    label: "Shot sizes",
    href: "#shot-sizes-and-framing"
  }, {
    label: "Angles",
    href: "#camera-angles"
  }, {
    label: "Composition",
    href: "#composition-techniques"
  }, {
    label: "Movements",
    href: "#camera-movements"
  }, {
    label: "Focus",
    href: "#focus-techniques"
  }, {
    label: "Lenses",
    href: "#lenses-and-optics"
  }, {
    label: "Shutter & time",
    href: "#shutter-and-time"
  }, {
    label: "Lighting",
    href: "#lighting-styles"
  }, {
    label: "Transitions",
    href: "#shot-transitions"
  }, {
    label: "POV",
    href: "#pov-and-specialty-rigs"
  }, {
    label: "Format",
    href: "#aspect-and-format"
  }, {
    label: "VFX",
    href: "#vfx-and-transformation"
  }, {
    label: "Art direction",
    href: "#art-direction"
  }, {
    label: "Animation",
    href: "#animation-and-media"
  }];
  const OFFSET = 64;
  const [active, setActive] = useState(SECTIONS[0].href);
  const [pinned, setPinned] = useState(false);
  const [box, setBox] = useState({
    left: 0,
    width: 0,
    height: 0
  });
  const [offset, setOffset] = useState(OFFSET);
  const wrapRef = useRef(null);
  const navRef = useRef(null);
  const listRef = useRef(null);
  const navbarRef = useRef(undefined);
  const navbarBottom = () => {
    if (typeof document === "undefined") return OFFSET;
    if (navbarRef.current === undefined) {
      const vw = document.documentElement.clientWidth;
      const found = Array.from(document.querySelectorAll("body div, header, nav")).find(el => {
        if (el.closest && el.closest(".camera-nav-wrap")) return false;
        const s = getComputedStyle(el);
        if (s.position !== "fixed" && s.position !== "sticky") return false;
        const r = el.getBoundingClientRect();
        return r.top <= 0.5 && r.width > vw * 0.7 && r.height > 20 && r.height < 200;
      });
      navbarRef.current = found || null;
    }
    const nb = navbarRef.current;
    if (!nb) return OFFSET;
    return Math.max(0, Math.round(nb.getBoundingClientRect().bottom));
  };
  const tickingRef = useRef(false);
  const scrollWiredRef = useRef(false);
  const measure = () => {
    const wrap = wrapRef.current;
    const nav = navRef.current;
    if (!wrap || !nav) return;
    const rect = wrap.getBoundingClientRect();
    const off = navbarBottom();
    setOffset(prev => prev === off ? prev : off);
    const shouldPin = rect.top <= off;
    setPinned(prev => prev === shouldPin ? prev : shouldPin);
    const left = Math.round(rect.left);
    const width = Math.round(rect.width);
    const height = nav.offsetHeight;
    setBox(prev => prev.left === left && prev.width === width && prev.height === height ? prev : {
      left,
      width,
      height
    });
    const vw = document.documentElement.clientWidth;
    const GUTTER = 40;
    const bleed = Math.max(0, Math.round(vw - rect.right - GUTTER));
    document.documentElement.style.setProperty("--camera-bleed", `${bleed}px`);
    updateActive(off + height + 24);
  };
  const onScroll = () => {
    if (tickingRef.current) return;
    tickingRef.current = true;
    requestAnimationFrame(() => {
      tickingRef.current = false;
      measure();
    });
  };
  const attachWrap = el => {
    wrapRef.current = el;
    if (!el || scrollWiredRef.current || typeof window === "undefined") return;
    scrollWiredRef.current = true;
    window.addEventListener("scroll", onScroll, true);
    window.addEventListener("resize", onScroll);
    if (typeof requestAnimationFrame !== "undefined") {
      requestAnimationFrame(measure);
    } else {
      measure();
    }
  };
  const centerPill = href => {
    const root = navRef.current;
    if (!root) return;
    const el = root.querySelector(`[data-href="${href}"]`);
    if (el && el.scrollIntoView) {
      el.scrollIntoView({
        block: "nearest",
        inline: "center",
        behavior: "smooth"
      });
    }
  };
  const activeRef = useRef(SECTIONS[0].href);
  const updateActive = line => {
    if (typeof document === "undefined") return;
    let best = SECTIONS[0].href;
    let bestTop = -Infinity;
    SECTIONS.forEach(s => {
      [s, ...s.children || []].forEach(item => {
        const node = document.getElementById(item.href.slice(1));
        if (!node) return;
        const top = node.getBoundingClientRect().top;
        if (top <= line && top > bestTop) {
          bestTop = top;
          best = item.href;
        }
      });
    });
    const doc = document.documentElement;
    if (window.innerHeight + window.scrollY >= doc.scrollHeight - 2) {
      const last = SECTIONS[SECTIONS.length - 1];
      const kids = last.children || [];
      const href = kids.length ? kids[kids.length - 1].href : last.href;
      const node = document.getElementById(href.slice(1));
      if (node && node.getBoundingClientRect().top < window.innerHeight) best = href;
    }
    if (best === activeRef.current) return;
    activeRef.current = best;
    setActive(best);
    centerPill(best);
  };
  const attachNav = el => {
    navRef.current = el;
  };
  const navStyle = pinned ? {
    position: "fixed",
    top: `${offset}px`,
    left: `${box.left}px`,
    width: `${box.width}px`
  } : undefined;
  const activeParent = SECTIONS.find(s => s.href === active || (s.children || []).some(c => c.href === active)) || SECTIONS[0];
  const subItems = activeParent.children || [];
  const isParentActive = s => s.href === active || (s.children || []).some(c => c.href === active);
  return <div className="camera-nav-wrap" ref={attachWrap} style={pinned ? {
    minHeight: `${box.height}px`
  } : undefined}>
      <nav className={pinned ? "not-prose camera-nav is-pinned" : "not-prose camera-nav"} ref={attachNav} style={navStyle} aria-label={label || "Camera term sections"}>
        <ul className="camera-nav__list" ref={listRef}>
          {SECTIONS.map(s => <li key={s.href}>
              <a href={s.href} data-href={s.href} className={isParentActive(s) ? "camera-nav__pill is-active" : "camera-nav__pill"} aria-current={isParentActive(s) ? "true" : undefined} onClick={() => {
    activeRef.current = s.href;
    setActive(s.href);
  }}>
                {s.label}
              </a>
            </li>)}
        </ul>

        {subItems.length > 0 && <ul className="camera-nav__list camera-nav__list--sub">
            {subItems.map(c => <li key={c.href}>
                <a href={c.href} data-href={c.href} className={active === c.href ? "camera-nav__pill camera-nav__pill--sub is-active" : "camera-nav__pill camera-nav__pill--sub"} aria-current={active === c.href ? "true" : undefined} onClick={() => {
    activeRef.current = c.href;
    setActive(c.href);
  }}>
                  {c.label}
                </a>
              </li>)}
          </ul>}
      </nav>
    </div>;
};

export const VideoExtension = ({src, poster, originalDuration = 5, aspectRatio = "16 / 9", accent = "#3ecf8e", alt = ""}) => {
  const videoRef = useRef(null);
  const [playing, setPlaying] = useState(true);
  const [muted, setMuted] = useState(true);
  const [dur, setDur] = useState(0);
  const [t, setT] = useState(0);
  const orig = dur ? Math.min(originalDuration, dur) : originalDuration;
  const split = dur ? Math.min(Math.max(orig / dur, 0.06), 0.94) : 0.5;
  const progress = dur ? Math.min(t / dur, 1) : 0;
  const inExt = dur > 0 && t >= orig - 0.05;
  const fmt = s => {
    s = Math.max(0, Math.round(s || 0));
    const m = Math.floor(s / 60);
    return `${m}:${String(s % 60).padStart(2, "0")}`;
  };
  const onMeta = e => setDur(e.currentTarget.duration || 0);
  const onTime = e => setT(e.currentTarget.currentTime || 0);
  const onPlay = () => setPlaying(true);
  const onPause = () => setPlaying(false);
  const togglePlay = () => {
    const v = videoRef.current;
    if (!v) return;
    if (v.paused) {
      const p = v.play?.();
      if (p && typeof p.catch === "function") p.catch(() => {});
    } else {
      v.pause();
    }
  };
  const toggleMute = () => {
    const v = videoRef.current;
    const next = !muted;
    setMuted(next);
    if (v) v.muted = next;
  };
  const seek = e => {
    const v = videoRef.current;
    if (!v || !dur) return;
    const r = e.currentTarget.getBoundingClientRect();
    const x = (e.clientX - r.left) / r.width;
    v.currentTime = Math.min(Math.max(x, 0), 1) * dur;
    setT(v.currentTime);
  };
  const neutral = a => `color-mix(in srgb, currentColor ${a}%, transparent)`;
  const iconBtn = {
    position: "absolute",
    top: "0.9rem",
    right: "0.9rem",
    zIndex: 4,
    display: "inline-flex",
    alignItems: "center",
    justifyContent: "center",
    width: "2.4rem",
    height: "2.4rem",
    padding: 0,
    borderRadius: "999px",
    border: "none",
    background: "rgba(12, 14, 18, 0.62)",
    backdropFilter: "blur(6px)",
    color: "#fff",
    cursor: "pointer",
    transition: "background 160ms ease"
  };
  return <div className="not-prose" style={{
    margin: "0.5rem 0 0.25rem"
  }}>
      {}
      <div onClick={togglePlay} style={{
    position: "relative",
    width: "100%",
    aspectRatio,
    borderRadius: "0.9rem",
    overflow: "hidden",
    background: "#0c0e12",
    cursor: "pointer",
    boxShadow: `0 0 0 1px ${neutral(10)}`
  }}>
        <video ref={videoRef} src={src} poster={poster} autoPlay loop muted={muted} playsInline preload="metadata" aria-label={alt} onLoadedMetadata={onMeta} onTimeUpdate={onTime} onPlay={onPlay} onPause={onPause} style={{
    position: "absolute",
    inset: 0,
    width: "100%",
    height: "100%",
    objectFit: "cover",
    display: "block"
  }} />

        {}
        <div style={{
    position: "absolute",
    top: "0.9rem",
    left: "0.9rem",
    zIndex: 4,
    display: "inline-flex",
    alignItems: "center",
    gap: "0.45rem",
    padding: "0.32rem 0.7rem 0.32rem 0.6rem",
    borderRadius: "999px",
    fontSize: "0.72rem",
    fontWeight: 600,
    letterSpacing: "0.01em",
    color: "#fff",
    background: inExt ? `color-mix(in srgb, ${accent} 82%, #0c0e12)` : "rgba(12, 14, 18, 0.62)",
    backdropFilter: "blur(6px)",
    boxShadow: inExt ? `0 0 0 1px ${accent}, 0 6px 20px -6px ${accent}` : `0 0 0 1px ${neutral(14)}`,
    transition: "background 320ms ease, box-shadow 320ms ease"
  }}>
          <span style={{
    width: "0.5rem",
    height: "0.5rem",
    borderRadius: "999px",
    background: inExt ? "#fff" : accent,
    boxShadow: inExt ? "0 0 8px #fff" : `0 0 8px ${accent}`,
    transition: "background 320ms ease"
  }} />
          {inExt ? "FLUX 3 continuation" : "Original clip"}
        </div>

        {}
        <button type="button" onClick={e => {
    e.stopPropagation();
    toggleMute();
  }} aria-label={muted ? "Unmute" : "Mute"} style={iconBtn}>
          {muted ? <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
              <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
              <line x1="23" y1="9" x2="17" y2="15" />
              <line x1="17" y1="9" x2="23" y2="15" />
            </svg> : <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
              <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
              <path d="M15.54 8.46a5 5 0 0 1 0 7.07" />
              <path d="M19.07 4.93a10 10 0 0 1 0 14.14" />
            </svg>}
        </button>

        {}
        <div style={{
    position: "absolute",
    inset: 0,
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
    zIndex: 3,
    pointerEvents: "none",
    opacity: playing ? 0 : 1,
    transition: "opacity 200ms ease",
    background: playing ? "transparent" : "rgba(0,0,0,0.18)"
  }}>
          <span style={{
    display: "inline-flex",
    alignItems: "center",
    justifyContent: "center",
    width: "3.4rem",
    height: "3.4rem",
    borderRadius: "999px",
    background: "rgba(12, 14, 18, 0.66)",
    backdropFilter: "blur(6px)",
    boxShadow: `0 0 0 1px ${neutral(16)}`,
    color: "#fff"
  }}>
            <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
              <path d="M8 5v14l11-7z" />
            </svg>
          </span>
        </div>
      </div>

      {}
      <div style={{
    marginTop: "0.85rem"
  }}>
        <div role="slider" aria-label="Scrub — original clip then FLUX 3 continuation" aria-valuemin={0} aria-valuemax={Math.round(dur)} aria-valuenow={Math.round(t)} onClick={seek} style={{
    position: "relative",
    height: "1.5rem",
    cursor: "pointer",
    display: "flex",
    alignItems: "center"
  }}>
          {}
          <div style={{
    position: "relative",
    width: "100%",
    height: "0.55rem",
    borderRadius: "999px",
    background: neutral(8),
    overflow: "visible"
  }}>
            {}
            <div style={{
    position: "absolute",
    left: 0,
    top: 0,
    bottom: 0,
    width: `${split * 100}%`,
    borderRadius: "999px 0 0 999px",
    background: neutral(16)
  }} />
            {}
            <div style={{
    position: "absolute",
    right: 0,
    top: 0,
    bottom: 0,
    width: `${(1 - split) * 100}%`,
    borderRadius: "0 999px 999px 0",
    background: `linear-gradient(90deg, color-mix(in srgb, ${accent} 42%, transparent), color-mix(in srgb, ${accent} 22%, transparent))`
  }} />
            {}
            <div style={{
    position: "absolute",
    left: 0,
    top: 0,
    bottom: 0,
    width: `${progress * 100}%`,
    borderRadius: "999px",
    background: neutral(24),
    transition: "width 120ms linear",
    pointerEvents: "none"
  }} />
            {}
            <div style={{
    position: "absolute",
    left: `${split * 100}%`,
    top: "-0.28rem",
    bottom: "-0.28rem",
    width: "2px",
    transform: "translateX(-1px)",
    background: accent,
    borderRadius: "2px",
    boxShadow: `0 0 8px ${accent}`
  }} />
            {}
            <div style={{
    position: "absolute",
    left: `${progress * 100}%`,
    top: "50%",
    width: "0.85rem",
    height: "0.85rem",
    marginLeft: "-0.425rem",
    marginTop: "-0.425rem",
    borderRadius: "999px",
    background: "#fff",
    boxShadow: `0 1px 6px rgba(0,0,0,0.45)${inExt ? `, 0 0 0 3px color-mix(in srgb, ${accent} 55%, transparent)` : ""}`,
    transition: "left 120ms linear, box-shadow 200ms ease",
    pointerEvents: "none"
  }} />
          </div>
        </div>

        {}
        <div style={{
    display: "flex",
    justifyContent: "space-between",
    alignItems: "center",
    marginTop: "0.55rem",
    fontSize: "0.78rem"
  }}>
          <span style={{
    display: "inline-flex",
    alignItems: "center",
    gap: "0.4rem",
    opacity: 0.75
  }}>
            <span style={{
    width: "0.65rem",
    height: "0.65rem",
    borderRadius: "3px",
    background: neutral(30)
  }} />
            Original {fmt(orig)}
          </span>
          <span style={{
    fontVariantNumeric: "tabular-nums",
    opacity: 0.55,
    fontSize: "0.74rem"
  }}>
            {fmt(t)} / {fmt(dur)}
          </span>
          <span style={{
    display: "inline-flex",
    alignItems: "center",
    gap: "0.4rem",
    color: accent,
    fontWeight: 600
  }}>
            <span style={{
    width: "0.65rem",
    height: "0.65rem",
    borderRadius: "3px",
    background: accent,
    boxShadow: `0 0 8px ${accent}`
  }} />
            FLUX 3 continuation +{fmt(Math.max(dur - orig, 0))}
          </span>
        </div>
      </div>
    </div>;
};

export const AnimateSlider = ({items = []}) => {
  const [inView, setInView] = useState(0);
  const [edges, setEdges] = useState({
    left: false,
    right: items.length > 1
  });
  const [muted, setMuted] = useState(true);
  const trackRef = useRef(null);
  const cardRefs = useRef([]);
  const slideRefs = useRef([]);
  const measuredRef = useRef(false);
  const mutedRef = useRef(true);
  const setSlideRef = (idx, key) => el => {
    if (!slideRefs.current[idx]) slideRefs.current[idx] = {};
    slideRefs.current[idx][key] = el;
  };
  const showStill = (idx, show) => {
    const s = slideRefs.current[idx];
    if (!s) return;
    if (s.still) s.still.style.opacity = show ? "1" : "0";
    if (s.pill) s.pill.style.opacity = show ? "1" : "0";
  };
  const stop = idx => {
    const s = slideRefs.current[idx];
    if (!s || !s.video) return;
    s.video.pause?.();
    try {
      s.video.currentTime = 0;
    } catch (e) {}
    showStill(idx, true);
  };
  const toggle = idx => {
    const s = slideRefs.current[idx];
    if (!s || !s.video) return;
    if (s.video.paused) {
      s.video.muted = mutedRef.current;
      showStill(idx, false);
      const p = s.video.play?.();
      if (p && typeof p.catch === "function") p.catch(() => {});
    } else {
      stop(idx);
    }
  };
  const observerRef = useRef(null);
  const getObserver = () => {
    if (observerRef.current) return observerRef.current;
    if (typeof IntersectionObserver === "undefined") return null;
    observerRef.current = new IntersectionObserver(entries => {
      entries.forEach(entry => {
        if (!entry.isIntersecting) {
          const idx = Number(entry.target.getAttribute("data-idx"));
          if (!Number.isNaN(idx)) stop(idx);
        }
      });
    }, {
      threshold: 0.25
    });
    return observerRef.current;
  };
  const attachMedia = idx => el => {
    setSlideRef(idx, "media")(el);
    if (!el) return;
    const io = getObserver();
    if (io) io.observe(el);
  };
  const toggleMute = e => {
    e.stopPropagation();
    const next = !muted;
    setMuted(next);
    mutedRef.current = next;
    slideRefs.current.forEach(s => {
      if (s && s.video) s.video.muted = next;
    });
  };
  const updateEdges = el => {
    if (!el) return;
    const max = el.scrollWidth - el.clientWidth;
    const next = {
      left: el.scrollLeft > 4,
      right: el.scrollLeft < max - 4
    };
    setEdges(prev => prev.left === next.left && prev.right === next.right ? prev : next);
    const cards = cardRefs.current.filter(Boolean);
    if (cards.length) {
      let best = 0;
      let bestDist = Infinity;
      cards.forEach((card, i) => {
        const dist = Math.abs(card.offsetLeft - el.scrollLeft - 4);
        if (dist < bestDist) {
          bestDist = dist;
          best = i;
        }
      });
      setInView(prev => prev === best ? prev : best);
    }
  };
  const attachTrack = el => {
    trackRef.current = el;
    if (!el || measuredRef.current) return;
    measuredRef.current = true;
    if (typeof requestAnimationFrame !== "undefined") {
      requestAnimationFrame(() => updateEdges(el));
    } else {
      updateEdges(el);
    }
  };
  const scrollByCard = dir => {
    const el = trackRef.current;
    if (!el) return;
    const first = cardRefs.current.find(Boolean);
    const cardW = first ? first.offsetWidth + 20 : el.clientWidth * 0.8;
    el.scrollBy({
      left: dir * cardW,
      behavior: "smooth"
    });
  };
  const scrollToCard = idx => {
    const el = trackRef.current;
    const card = cardRefs.current[idx];
    if (!el || !card) return;
    el.scrollTo({
      left: card.offsetLeft - 4,
      behavior: "smooth"
    });
  };
  const muteBtnStyle = {
    position: "absolute",
    top: "0.6rem",
    right: "0.6rem",
    zIndex: 4,
    width: "2rem",
    height: "2rem",
    borderRadius: "999px",
    border: "none",
    background: "rgba(8, 18, 13, 0.5)",
    color: "#fff",
    display: "inline-flex",
    alignItems: "center",
    justifyContent: "center",
    cursor: "pointer",
    backdropFilter: "blur(4px)",
    WebkitBackdropFilter: "blur(4px)"
  };
  return <div className="not-prose clip-slider">
      <button type="button" className="clip-slider__arrow clip-slider__arrow--prev" onClick={() => scrollByCard(-1)} disabled={!edges.left} aria-label="Previous clip">
        <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
          <polyline points="15 18 9 12 15 6" />
        </svg>
      </button>

      <div className="clip-slider__track" ref={attachTrack} onScroll={e => updateEdges(e.currentTarget)}>
        {items.map((item, idx) => <article key={idx} className="clip-slider__slide" ref={el => {
    cardRefs.current[idx] = el;
  }}>
            {item.title && <h4 className="clip-slider__title">{item.title}</h4>}
            <div className="clip-slider__media" data-idx={idx} role="button" tabIndex={0} aria-label="Click to animate" ref={attachMedia(idx)} onClick={() => toggle(idx)} onKeyDown={e => {
    if (e.key === "Enter" || e.key === " ") {
      e.preventDefault();
      toggle(idx);
    }
  }} style={{
    cursor: "pointer",
    userSelect: "none",
    WebkitTapHighlightColor: "transparent"
  }}>
              <video ref={setSlideRef(idx, "video")} src={item.video} poster={item.image} loop muted playsInline preload="metadata" onEnded={() => stop(idx)} aria-label={item.alt || item.title || ""} className="clip-slider__media-el" />
              {}
              <img ref={setSlideRef(idx, "still")} src={item.image} alt={item.alt || ""} draggable={false} style={{
    position: "absolute",
    inset: 0,
    width: "100%",
    height: "100%",
    objectFit: "cover",
    display: "block",
    opacity: 1,
    transition: "opacity 240ms ease",
    pointerEvents: "none"
  }} />

              {}
              <span ref={setSlideRef(idx, "pill")} style={{
    position: "absolute",
    top: "0.6rem",
    left: "0.6rem",
    zIndex: 4,
    display: "inline-flex",
    alignItems: "center",
    gap: "0.45rem",
    padding: "0.35rem 0.7rem",
    borderRadius: "999px",
    background: "rgba(12, 14, 18, 0.62)",
    backdropFilter: "blur(6px)",
    WebkitBackdropFilter: "blur(6px)",
    color: "#fff",
    fontFamily: '"Instrument Sans", sans-serif',
    fontSize: "0.78rem",
    fontWeight: 600,
    letterSpacing: "-0.01em",
    pointerEvents: "none",
    opacity: 1,
    transition: "opacity 200ms ease"
  }}>
                <span style={{
    width: "0.5rem",
    height: "0.5rem",
    borderRadius: "999px",
    background: "#ff4438",
    boxShadow: "0 0 0 4px rgba(255, 68, 56, 0.25)"
  }} />
                Click to animate
              </span>

              <button type="button" onClick={toggleMute} aria-label={muted ? "Unmute" : "Mute"} title={muted ? "Enable sound" : "Mute"} style={muteBtnStyle}>
                {muted ? <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M11 5 6 9H2v6h4l5 4V5z" /><line x1="23" y1="9" x2="17" y2="15" /><line x1="17" y1="9" x2="23" y2="15" /></svg> : <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M11 5 6 9H2v6h4l5 4V5z" /><path d="M15.54 8.46a5 5 0 0 1 0 7.07" /><path d="M19.07 4.93a10 10 0 0 1 0 14.14" /></svg>}
              </button>
            </div>

            {item.prompt && <div className="clip-slider__body">
                <div className="clip-slider__prompt">
                  <pre className="clip-slider__prompt-pre"><code>{item.prompt}</code></pre>
                </div>
              </div>}
          </article>)}
      </div>

      <button type="button" className="clip-slider__arrow clip-slider__arrow--next" onClick={() => scrollByCard(1)} disabled={!edges.right} aria-label="Next clip">
        <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
          <polyline points="9 18 15 12 9 6" />
        </svg>
      </button>

      {items.length > 1 && <div className="clip-slider__dots">
          {items.map((item, idx) => <button key={idx} type="button" onClick={() => scrollToCard(idx)} aria-label={`Go to ${item.title || `clip ${idx + 1}`}`} className={`clip-slider__dot${idx === inView ? " is-active" : ""}`} />)}
        </div>}
    </div>;
};

export const ClipSlider = ({items = []}) => {
  const [copied, setCopied] = useState(-1);
  const [inView, setInView] = useState(0);
  const [edges, setEdges] = useState({
    left: false,
    right: items.length > 1
  });
  const [muted, setMuted] = useState(true);
  const [viewer, setViewer] = useState(null);
  const [split, setSplit] = useState({});
  const splitAt = idx => split[idx] === undefined ? items[idx] && items[idx].split !== undefined ? items[idx].split : 50 : split[idx];
  const moveSplit = (idx, e) => {
    const r = e.currentTarget.getBoundingClientRect();
    const pct = Math.max(0, Math.min(100, (e.clientX - r.left) / r.width * 100));
    setSplit(s => ({
      ...s,
      [idx]: pct
    }));
  };
  const BOX_COLORS = ["#e8b04a", "#6fb3d9", "#e0708a", "#8fcf7a", "#b99af0", "#f08b5b", "#5fd0c0", "#d9d36f", "#9fb0c8", "#f2a3c7", "#7aa0f0", "#c7e07a"];
  const fitBox = ([y0, x0, y1, x1], w, h) => {
    const frame = 16 / 9, ar = w / h;
    const sx = ar > frame ? 1 : ar / frame, sy = ar > frame ? frame / ar : 1;
    const ox = (1 - sx) / 2, oy = (1 - sy) / 2;
    return {
      top: (oy + y0 / 1000 * sy) * 100 + "%",
      left: (ox + x0 / 1000 * sx) * 100 + "%",
      height: (y1 - y0) / 1000 * sy * 100 + "%",
      width: (x1 - x0) / 1000 * sx * 100 + "%"
    };
  };
  const [actualSize, setActualSize] = useState(false);
  const trackRef = useRef(null);
  const cardRefs = useRef([]);
  const measuredRef = useRef(false);
  const mutedRef = useRef(true);
  const videosRef = useRef([]);
  const observerRef = useRef(null);
  const getObserver = () => {
    if (observerRef.current) return observerRef.current;
    if (typeof IntersectionObserver === "undefined") return null;
    observerRef.current = new IntersectionObserver(entries => {
      entries.forEach(entry => {
        const v = entry.target;
        if (entry.isIntersecting) {
          v.muted = mutedRef.current;
          const p = v.play?.();
          if (p && typeof p.catch === "function") p.catch(() => {});
        } else {
          v.pause?.();
        }
      });
    }, {
      threshold: 0.4
    });
    return observerRef.current;
  };
  const attachVideo = el => {
    if (!el) return;
    if (!videosRef.current.includes(el)) videosRef.current.push(el);
    const io = getObserver();
    if (io) io.observe(el);
  };
  const toggleMute = () => {
    const next = !muted;
    setMuted(next);
    mutedRef.current = next;
    videosRef.current.forEach(v => {
      if (v) v.muted = next;
    });
  };
  const updateEdges = el => {
    if (!el) return;
    const max = el.scrollWidth - el.clientWidth;
    const next = {
      left: el.scrollLeft > 4,
      right: el.scrollLeft < max - 4
    };
    setEdges(prev => prev.left === next.left && prev.right === next.right ? prev : next);
    const cards = cardRefs.current.filter(Boolean);
    if (cards.length) {
      let best = 0;
      let bestDist = Infinity;
      cards.forEach((card, i) => {
        const dist = Math.abs(card.offsetLeft - el.scrollLeft - 4);
        if (dist < bestDist) {
          bestDist = dist;
          best = i;
        }
      });
      setInView(prev => prev === best ? prev : best);
    }
  };
  const attachTrack = el => {
    trackRef.current = el;
    if (!el || measuredRef.current) return;
    measuredRef.current = true;
    if (typeof requestAnimationFrame !== "undefined") {
      requestAnimationFrame(() => updateEdges(el));
    } else {
      updateEdges(el);
    }
  };
  const scrollByCard = dir => {
    const el = trackRef.current;
    if (!el) return;
    const first = cardRefs.current.find(Boolean);
    const cardW = first ? first.offsetWidth + 20 : el.clientWidth * 0.8;
    el.scrollBy({
      left: dir * cardW,
      behavior: "smooth"
    });
  };
  const scrollToCard = idx => {
    const el = trackRef.current;
    const card = cardRefs.current[idx];
    if (!el || !card) return;
    el.scrollTo({
      left: card.offsetLeft - 4,
      behavior: "smooth"
    });
  };
  const startVideo = event => {
    const video = event.currentTarget;
    video.muted = mutedRef.current;
    const p = video.play?.();
    if (p && typeof p.catch === "function") p.catch(() => {});
  };
  const copy = (text, idx) => {
    try {
      navigator.clipboard.writeText(text);
      setCopied(idx);
      setTimeout(() => setCopied(c => c === idx ? -1 : c), 2000);
    } catch (e) {}
  };
  return <div className="not-prose clip-slider">
      <button type="button" className="clip-slider__arrow clip-slider__arrow--prev" onClick={() => scrollByCard(-1)} disabled={!edges.left} aria-label="Previous clip">
        <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
          <polyline points="15 18 9 12 15 6" />
        </svg>
      </button>

      <div className="clip-slider__track" ref={attachTrack} onScroll={e => updateEdges(e.currentTarget)}>
        {items.map((item, idx) => <article key={idx} className="clip-slider__slide" ref={el => {
    cardRefs.current[idx] = el;
  }}>
            {item.title && <h4 className="clip-slider__title">{item.title}</h4>}
            <div className={item.boxes ? "clip-slider__media has-boxes" : item.fit === "contain" ? "clip-slider__media is-fit" : item.before ? "clip-slider__media has-compare" : "clip-slider__media"}>
              {item.before ? <div className="clip-slider__compare" role="slider" tabIndex={0} aria-label={`Compare before and after: ${item.alt || ""}`} aria-valuemin={0} aria-valuemax={100} aria-valuenow={Math.round(splitAt(idx))} onPointerDown={e => {
    e.currentTarget.setPointerCapture?.(e.pointerId);
    moveSplit(idx, e);
  }} onPointerMove={e => {
    if (e.buttons) moveSplit(idx, e);
  }} onKeyDown={e => {
    if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
      e.preventDefault();
      setSplit(s => ({
        ...s,
        [idx]: Math.max(0, Math.min(100, splitAt(idx) + (e.key === "ArrowLeft" ? -5 : 5)))
      }));
    }
  }}>
                  <img src={item.after} alt={item.alt ? "After: " + item.alt : ""} loading="lazy" className="clip-slider__media-el" draggable={false} />
                  <div className="clip-slider__before" style={{
    clipPath: `inset(0 ${100 - splitAt(idx)}% 0 0)`
  }}>
                    <img src={item.before} alt={item.alt ? "Before: " + item.alt : ""} loading="lazy" className="clip-slider__media-el" draggable={false} />
                  </div>
                  <span className="clip-slider__handle" style={{
    left: splitAt(idx) + "%"
  }} aria-hidden="true">
                    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><polyline points="9 18 3 12 9 6" /><polyline points="15 6 21 12 15 18" /></svg>
                  </span>
                  <span className="clip-slider__tag clip-slider__tag--before" aria-hidden="true">Before</span>
                  <span className="clip-slider__tag clip-slider__tag--after" aria-hidden="true">After</span>
                </div> : item.image ? <button type="button" className="clip-slider__zoom" onClick={() => {
    setActualSize(false);
    setViewer(idx);
  }} aria-label={`View full size: ${item.alt || item.title || ""}`}>
                  <img src={item.image} alt={item.alt || item.title || ""} loading="lazy" className="clip-slider__media-el" />
                  {item.boxes && item.boxes.map((b, k) => <span key={b.id} className={"clip-slider__box" + (b.bbox[0] < 60 ? " is-top" : "")} style={{
    ...fitBox(b.bbox, item.w, item.h),
    "--c": BOX_COLORS[k % BOX_COLORS.length]
  }}>
                      <i>{b.id}</i>
                    </span>)}
                </button> : <>
              <video ref={attachVideo} src={item.video} poster={item.poster} loop muted playsInline preload="metadata" onLoadedData={startVideo} onCanPlay={startVideo} aria-label={item.alt || item.title || ""} className="clip-slider__media-el" />
              <button type="button" onClick={toggleMute} aria-label={muted ? "Unmute" : "Mute"} title={muted ? "Enable sound" : "Mute"} style={{
    position: "absolute",
    top: "0.6rem",
    right: "0.6rem",
    zIndex: 4,
    width: "2rem",
    height: "2rem",
    borderRadius: "999px",
    border: "none",
    background: "rgba(8, 18, 13, 0.5)",
    color: "#fff",
    display: "inline-flex",
    alignItems: "center",
    justifyContent: "center",
    cursor: "pointer",
    backdropFilter: "blur(4px)",
    WebkitBackdropFilter: "blur(4px)"
  }}>
                {muted ? <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M11 5 6 9H2v6h4l5 4V5z" /><line x1="23" y1="9" x2="17" y2="15" /><line x1="17" y1="9" x2="23" y2="15" /></svg> : <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M11 5 6 9H2v6h4l5 4V5z" /><path d="M15.54 8.46a5 5 0 0 1 0 7.07" /><path d="M19.07 4.93a10 10 0 0 1 0 14.14" /></svg>}
              </button>
              </>}
            </div>

            {item.prompt && <div className="clip-slider__body">
                <div className="clip-slider__prompt">
                  <button type="button" className="clip-slider__copy" onClick={() => copy(item.prompt, idx)} aria-label={copied === idx ? "Prompt copied" : "Copy prompt"} title={copied === idx ? "Copied" : "Copy prompt"}>
                    {copied === idx ? "Copied!" : "Copy"}
                  </button>
                  <pre className="clip-slider__prompt-pre"><code>{item.prompt}</code></pre>
                </div>
              </div>}
          </article>)}
      </div>

      <button type="button" className="clip-slider__arrow clip-slider__arrow--next" onClick={() => scrollByCard(1)} disabled={!edges.right} aria-label="Next clip">
        <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
          <polyline points="9 18 15 12 9 6" />
        </svg>
      </button>

      {items.length > 1 && <div className="clip-slider__dots">
          {items.map((item, idx) => <button key={idx} type="button" onClick={() => scrollToCard(idx)} aria-label={`Go to ${item.title || `clip ${idx + 1}`}`} className={`clip-slider__dot${idx === inView ? " is-active" : ""}`} />)}
        </div>}

      {viewer !== null && items[viewer] ? <dialog className={actualSize ? "clip-slider__viewer" : "clip-slider__viewer is-fit"} aria-label={items[viewer].alt || items[viewer].title || ""} ref={node => {
    if (node && !node.open) node.showModal();
  }} onClose={() => setViewer(null)} onClick={() => setViewer(null)}>
          <img src={items[viewer].full || items[viewer].image} alt={items[viewer].alt || items[viewer].title || ""} style={{
    cursor: actualSize ? "zoom-out" : "zoom-in"
  }} onClick={event => {
    event.stopPropagation();
    setActualSize(!actualSize);
  }} />
          <div className="clip-slider__viewer-bar" onClick={event => event.stopPropagation()}>
            <button type="button" onClick={() => setActualSize(!actualSize)}>
              {actualSize ? "Fit to screen" : "Actual size"}
            </button>
            <button type="button" onClick={() => setViewer(null)}>Close</button>
          </div>
        </dialog> : null}
    </div>;
};

export const MutedVideo = ({src, poster, alt = "", aspectRatio = "16 / 9", borderRadius = "0.75rem"}) => {
  const videoRef = useRef(null);
  const [muted, setMuted] = useState(true);
  const start = event => {
    const v = event.currentTarget;
    v.muted = muted;
    const p = v.play?.();
    if (p && typeof p.catch === "function") p.catch(() => {});
  };
  const toggleMute = () => {
    const v = videoRef.current;
    const next = !muted;
    setMuted(next);
    if (v) v.muted = next;
  };
  const iconBtn = {
    position: "absolute",
    top: "0.9rem",
    right: "0.9rem",
    zIndex: 3,
    display: "inline-flex",
    alignItems: "center",
    justifyContent: "center",
    width: "2.4rem",
    height: "2.4rem",
    padding: 0,
    borderRadius: "999px",
    border: "none",
    background: "rgba(12, 14, 18, 0.62)",
    backdropFilter: "blur(6px)",
    color: "#fff",
    cursor: "pointer",
    transition: "background 160ms ease"
  };
  return <div className="not-prose" style={{
    position: "relative",
    width: "100%",
    aspectRatio,
    borderRadius,
    overflow: "hidden",
    background: "#0c0e12"
  }}>
      <video ref={videoRef} src={src} poster={poster} autoPlay loop muted={muted} playsInline preload="metadata" onCanPlay={start} onLoadedData={start} aria-label={alt} style={{
    position: "absolute",
    inset: 0,
    width: "100%",
    height: "100%",
    objectFit: "cover",
    display: "block"
  }} />
      <button type="button" onClick={toggleMute} aria-label={muted ? "Unmute" : "Mute"} style={iconBtn}>
        {muted ? <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
            <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
            <line x1="23" y1="9" x2="17" y2="15" />
            <line x1="17" y1="9" x2="23" y2="15" />
          </svg> : <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
            <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
            <path d="M15.54 8.46a5 5 0 0 1 0 7.07" />
            <path d="M19.07 4.93a10 10 0 0 1 0 14.14" />
          </svg>}
      </button>
    </div>;
};

export const VideoComparisonSlider = ({beforeVideo, afterVideo, beforeLabel = "Before", afterLabel = "After", height = "500px", objectFit = "cover", poster}) => {
  const makeLogic = () => {
    const STEP = 2;
    const PAGE_STEP = 10;
    const clampPercent = value => Number.isFinite(value) ? Math.max(0, Math.min(100, value)) : 0;
    const percentFromX = (clientX, rect) => {
      if (!rect || !rect.width || !Number.isFinite(clientX)) return 0;
      return clampPercent((clientX - rect.left) / rect.width * 100);
    };
    const keyPosition = (key, current) => {
      const at = clampPercent(current);
      if (key === "ArrowLeft" || key === "ArrowDown") return clampPercent(at - STEP);
      if (key === "ArrowRight" || key === "ArrowUp") return clampPercent(at + STEP);
      if (key === "PageDown") return clampPercent(at - PAGE_STEP);
      if (key === "PageUp") return clampPercent(at + PAGE_STEP);
      if (key === "Home") return 0;
      if (key === "End") return 100;
      return null;
    };
    const valueText = (position, before, after) => {
      const left = Math.round(clampPercent(position));
      return `${left}% ${before}, ${100 - left}% ${after}`;
    };
    return {
      clampPercent,
      percentFromX,
      keyPosition,
      valueText
    };
  };
  const L = makeLogic();
  const [position, setPosition] = useState(50);
  const [playing, setPlaying] = useState(true);
  const beforeRef = useRef(null);
  const afterRef = useRef(null);
  const userPausedRef = useRef(false);
  const lastAutoResumeRef = useRef(0);
  const DRIFT_S = 0.04;
  const SEEK_S = 0.5;
  const readPosition = (e, container) => {
    const rect = container.getBoundingClientRect();
    const clientX = e.touches ? e.touches[0].clientX : e.clientX;
    return L.percentFromX(clientX, rect);
  };
  const onPointerDown = e => {
    e.preventDefault();
    e.stopPropagation();
    const container = e.currentTarget;
    if (typeof container.focus === "function") container.focus();
    setPosition(readPosition(e, container));
    const onMove = ev => {
      ev.preventDefault();
      setPosition(readPosition(ev, container));
    };
    const onUp = () => {
      window.removeEventListener("mousemove", onMove);
      window.removeEventListener("mouseup", onUp);
      window.removeEventListener("touchmove", onMove);
      window.removeEventListener("touchend", onUp);
    };
    window.addEventListener("mousemove", onMove);
    window.addEventListener("mouseup", onUp);
    window.addEventListener("touchmove", onMove, {
      passive: false
    });
    window.addEventListener("touchend", onUp);
  };
  const onKeyDown = e => {
    const next = L.keyPosition(e.key, position);
    if (next === null) return;
    e.preventDefault();
    e.stopPropagation();
    setPosition(next);
  };
  const loopPoint = () => {
    const a = beforeRef.current;
    const b = afterRef.current;
    if (!a || !b) return 0;
    const da = Number.isFinite(a.duration) ? a.duration : 0;
    const db = Number.isFinite(b.duration) ? b.duration : 0;
    if (!da || !db) return da || db;
    return Math.min(da, db);
  };
  const resync = force => {
    const a = beforeRef.current;
    const b = afterRef.current;
    if (!a || !b) return;
    if (b.readyState < 1) return;
    const delta = b.currentTime - a.currentTime;
    if (force || Math.abs(delta) > SEEK_S) {
      try {
        b.currentTime = a.currentTime;
      } catch (e) {}
      b.playbackRate = 1;
    } else if (Math.abs(delta) > DRIFT_S) {
      const rate = 1 - Math.max(-0.08, Math.min(0.08, delta * 0.5));
      b.playbackRate = rate;
    } else {
      b.playbackRate = 1;
    }
    if (a.paused && !b.paused) b.pause();
    if (!a.paused && b.paused) {
      const p = b.play?.();
      if (p && typeof p.catch === "function") p.catch(() => {});
    }
  };
  const restart = () => {
    if (userPausedRef.current) return;
    const a = beforeRef.current;
    const b = afterRef.current;
    if (!a) return;
    try {
      a.currentTime = 0;
    } catch (e) {}
    if (b) {
      try {
        b.currentTime = 0;
      } catch (e) {}
    }
    const p = a.play?.();
    if (p && typeof p.catch === "function") p.catch(() => {});
    if (b) {
      const q = b.play?.();
      if (q && typeof q.catch === "function") q.catch(() => {});
    }
  };
  const setBeforeRef = el => {
    beforeRef.current = el;
    if (!el || el.dataset.vcsWired === "1") return;
    el.dataset.vcsWired = "1";
    el.addEventListener("timeupdate", () => {
      const end = loopPoint();
      if (end && el.currentTime >= end - 0.12) {
        restart();
        return;
      }
      resync(false);
    });
    el.addEventListener("ended", restart);
    el.addEventListener("seeked", () => resync(true));
    el.addEventListener("play", () => {
      if (userPausedRef.current) {
        el.pause();
        return;
      }
      setPlaying(true);
      resync(true);
    });
    el.addEventListener("pause", () => {
      const rect = el.getBoundingClientRect();
      const inView = rect.bottom > 0 && rect.top < window.innerHeight && rect.right > 0 && rect.left < window.innerWidth;
      const now = Date.now();
      if (!userPausedRef.current && !el.ended && !document.hidden && inView && now - lastAutoResumeRef.current > 250) {
        lastAutoResumeRef.current = now;
        const p = el.play?.();
        if (p && typeof p.catch === "function") {
          p.catch(() => {
            setPlaying(false);
            resync(true);
          });
        }
        return;
      }
      setPlaying(false);
      resync(true);
    });
    el.addEventListener("loadeddata", () => resync(true));
  };
  const setAfterRef = el => {
    afterRef.current = el;
    if (!el || el.dataset.vcsWired === "1") return;
    el.dataset.vcsWired = "1";
    el.addEventListener("play", () => {
      if (userPausedRef.current) el.pause();
    });
    el.addEventListener("loadeddata", () => resync(true));
  };
  const togglePlay = e => {
    e.preventDefault();
    e.stopPropagation();
    const a = beforeRef.current;
    if (!a) return;
    if (!playing) {
      userPausedRef.current = false;
      const p = a.play?.();
      if (p && typeof p.catch === "function") p.catch(() => {});
    } else {
      userPausedRef.current = true;
      setPlaying(false);
      a.pause();
    }
    resync(true);
  };
  const videoStyle = {
    position: "absolute",
    top: 0,
    left: 0,
    width: "100%",
    height: "100%",
    objectFit,
    pointerEvents: "none"
  };
  const labelStyle = {
    position: "absolute",
    top: "12px",
    padding: "4px 10px",
    borderRadius: "6px",
    background: "rgba(0,0,0,0.55)",
    backdropFilter: "blur(4px)",
    color: "#fff",
    fontSize: "0.75rem",
    fontWeight: 600,
    letterSpacing: "0.02em",
    pointerEvents: "none",
    zIndex: 4
  };
  return <div className="not-prose" style={{
    position: "relative",
    borderRadius: "1rem",
    overflow: "hidden",
    height,
    width: "100%"
  }}>
      <div role="slider" tabIndex={0} aria-label={`Comparison position between ${beforeLabel} and ${afterLabel}`} aria-valuemin={0} aria-valuemax={100} aria-valuenow={Math.round(position)} aria-valuetext={L.valueText(position, beforeLabel, afterLabel)} onMouseDown={onPointerDown} onTouchStart={onPointerDown} onKeyDown={onKeyDown} onClick={e => {
    e.preventDefault();
    e.stopPropagation();
  }} style={{
    position: "relative",
    width: "100%",
    height,
    overflow: "hidden",
    cursor: "ew-resize",
    userSelect: "none",
    WebkitUserSelect: "none",
    background: "#000",
    outlineOffset: "-3px"
  }}>
        {}
        <video ref={setAfterRef} src={afterVideo} poster={poster} autoPlay muted playsInline preload="auto" aria-label={afterLabel} style={videoStyle} />

        {}
        <div style={{
    position: "absolute",
    top: 0,
    left: 0,
    width: "100%",
    height: "100%",
    clipPath: `inset(0 ${100 - position}% 0 0)`
  }}>
          <video ref={setBeforeRef} src={beforeVideo} poster={poster} autoPlay muted playsInline preload="auto" aria-label={beforeLabel} style={videoStyle} />
        </div>

        {}
        <div style={{
    position: "absolute",
    top: 0,
    left: `${position}%`,
    transform: "translateX(-50%)",
    width: "3px",
    height: "100%",
    background: "rgba(255,255,255,0.85)",
    pointerEvents: "none",
    zIndex: 2
  }} />

        {}
        <div style={{
    position: "absolute",
    top: "50%",
    left: `${position}%`,
    transform: "translate(-50%, -50%)",
    width: "44px",
    height: "44px",
    borderRadius: "50%",
    background: "rgba(255,255,255,0.95)",
    border: "2px solid rgba(0,0,0,0.15)",
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
    gap: "6px",
    zIndex: 3,
    pointerEvents: "none",
    boxShadow: "0 2px 8px rgba(0,0,0,0.3)"
  }}>
          <svg width="10" height="14" viewBox="0 0 10 14" fill="none" style={{
    marginRight: "-2px"
  }}>
            <path d="M8 1L2 7L8 13" stroke="#333" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
          </svg>
          <svg width="10" height="14" viewBox="0 0 10 14" fill="none" style={{
    marginLeft: "-2px"
  }}>
            <path d="M2 1L8 7L2 13" stroke="#333" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
          </svg>
        </div>

        {}
        <div style={{
    ...labelStyle,
    left: "12px"
  }}>{beforeLabel}</div>
        <div style={{
    ...labelStyle,
    right: "12px"
  }}>{afterLabel}</div>
      </div>

      {}
      <button type="button" onMouseDown={e => e.stopPropagation()} onTouchStart={e => e.stopPropagation()} onClick={togglePlay} aria-label={playing ? "Pause both clips" : "Play both clips"} title={playing ? "Pause" : "Play"} style={{
    position: "absolute",
    bottom: "12px",
    left: "12px",
    width: "36px",
    height: "36px",
    padding: 0,
    border: "none",
    borderRadius: "50%",
    background: "rgba(0,0,0,0.55)",
    backdropFilter: "blur(4px)",
    color: "#fff",
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
    cursor: "pointer",
    zIndex: 5
  }}>
        {playing ? <svg width="12" height="14" viewBox="0 0 12 14" fill="none" aria-hidden="true">
            <rect x="1" y="1" width="3.5" height="12" rx="1" fill="#fff" />
            <rect x="7.5" y="1" width="3.5" height="12" rx="1" fill="#fff" />
          </svg> : <svg width="12" height="14" viewBox="0 0 12 14" fill="none" aria-hidden="true">
            <path d="M2 1.5L11 7L2 12.5V1.5Z" fill="#fff" />
          </svg>}
      </button>
    </div>;
};

export const PromptDisplay = ({prompt}) => {
  const [copied, setCopied] = useState(false);
  const copy = () => {
    navigator.clipboard.writeText(prompt);
    setCopied(true);
    setTimeout(() => setCopied(false), 2000);
  };
  return <div className="not-prose" style={{
    marginTop: "1rem"
  }}>
      <div style={{
    backgroundColor: "#1a1a1a",
    borderRadius: "1rem",
    padding: "1.25rem 1.5rem",
    display: "flex",
    flexDirection: "column",
    gap: "1rem"
  }}>
        <p style={{
    color: "#e5e5e5",
    fontSize: "1rem",
    lineHeight: 1.6,
    margin: 0,
    fontFamily: "inherit"
  }}>
          {prompt}
        </p>
        <div style={{
    display: "flex",
    justifyContent: "flex-end"
  }}>
          <button onClick={copy} style={{
    backgroundColor: copied ? "#3d8a5b" : "var(--aspen-evergreen, #486A58)",
    color: "#fff",
    border: "none",
    borderRadius: "0.375rem",
    padding: "0.25rem 0.6rem",
    fontSize: "0.7rem",
    fontWeight: 600,
    cursor: "pointer",
    transition: "background-color 0.2s"
  }}>
            {copied ? "Copied!" : "Copy prompt"}
          </button>
        </div>
      </div>
    </div>;
};

<div className="flux3-video-page flux3-page">
  <div className="not-prose flux3-video-hero cinematic-hero-root">
    <img src="https://cdn.sanity.io/images/2gpum2i6/production/8559a03b362942fe81cd9de2c2c6e633cbc1b494-2400x1166.png" alt="FLUX 3 Motion from a single line artwork" className="flux3-video-hero__image" />
  </div>

  <div className="not-prose flux3-video-copy">
    <p className="flux3-video-copy__eyebrow">FLUX 3</p>
    <h1>Video</h1>

    <p>
      Generate, animate, and continue video with FLUX 3, with
      synchronized audio in the same call.
    </p>
  </div>

  <CameraTermNav
    label="Page sections"
    sections={[
{ label: "Quickstart", href: "#quickstart" },
{ label: "Modes", href: "#modes" },
{ label: "Parameters", href: "#parameters" },
{ label: "Use cases", href: "#use-cases", children: [
  { label: "Text to video", href: "#text-to-video" },
  { label: "Image to video", href: "#image-to-video" },
  { label: "Multiple scenes", href: "#multiple-scenes" },
  { label: "Multilingual dialogue", href: "#multilingual-dialogue" },
  { label: "Broad range of styles", href: "#broad-range-of-styles" },
  { label: "Text and typography", href: "#text-and-typography" },
  { label: "Keyframes", href: "#keyframes" },
  { label: "Video continuation", href: "#video-continuation" },
  { label: "Video editing", href: "#video-editing" },
] },
{ label: "Keep exploring", href: "#keep-exploring" },
]}
  />

  ## Quickstart

  FLUX 3 video is asynchronous: you **submit** a request, get back a `polling_url`,
  and **poll** it until the clip is `Ready`. For the full field list and response
  schema, see the [API reference](/api-reference/utility/generate-a-video-with-flux-3).

  <CodeGroup>
    ```python Python theme={null}
    import os, time, requests

    BFL_API_KEY = os.environ["BFL_API_KEY"]

    # 1. Submit: returns an id and a polling_url
    submit = requests.post(
        "https://api.bfl.ai/v1/flux-3-video",
        headers={"x-key": BFL_API_KEY, "Content-Type": "application/json"},
        json={
            "mode": "t2v",
            "prompt": "a fox running through dawn mist",
            "generate_audio": True,
        },
    ).json()

    # 2. Poll the returned URL until the job is Ready
    while True:
        time.sleep(2)
        result = requests.get(submit["polling_url"], headers={"x-key": BFL_API_KEY}).json()
        if result["status"] == "Ready":
            print(result["result"]["sample"])   # signed .mp4 URL
            break
        if result["status"] in ("Error", "Request Moderated", "Content Moderated"):
            raise RuntimeError(result["status"])
    ```

    ```bash cURL theme={null}
    # 1. Submit: returns an id and a polling_url
    curl -s -X POST https://api.bfl.ai/v1/flux-3-video \
      -H "x-key: $BFL_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "mode": "t2v",
        "prompt": "a fox running through dawn mist",
        "generate_audio": true
      }'

    # 2. Poll the polling_url from the response until "status": "Ready"
    curl -s "$POLLING_URL" -H "x-key: $BFL_API_KEY"
    ```
  </CodeGroup>

  ## Modes

  Each `/v1/flux-3-video` request names a `mode`. Media fields and accepted parameter values depend
  on that mode; check the API reference before reusing a request across modes.

  * **Text-to-Video** (`t2v`): prompt only.
  * **Image-to-Video** (`i2v`): add `keyframes`: one image is the opening frame, `[a, b]` pins start and end, and up to ten frames pinned to timestamps become a storyboard.
  * **Video Continuation** (`v2v`): add `start_video`; the clip continues from its final frames.

  For editing, use the separate FLUX Video Edit \[fast] tool at
  `/v1/flux-tools/video-edit-v1`, documented on its [own page](/flux_tools/flux_video_edit).
  Send `video` and `prompt`, not a `mode`.

  <div className="not-prose" style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(min(100%, 15rem), 1fr))", gap: "1.25rem", margin: "1.25rem 0 0.5rem" }}>
    <div>
      <MutedVideo src="https://cdn.sanity.io/files/2gpum2i6/production/fc743c980a0259403ccec6c9a705f9e412e6f81b.mp4" alt="Text-to-video example" />

      <p style={{ margin: "0.6rem 0 0", fontSize: "0.9rem", opacity: 0.75 }}><code>t2v</code>: Generate a video with audio</p>
    </div>

    <div>
      <MutedVideo src="https://cdn.sanity.io/files/2gpum2i6/production/9b7469e7f349b4f26fe9809b9faa13567504aa84.mp4" alt="Image-to-video example" />

      <p style={{ margin: "0.6rem 0 0", fontSize: "0.9rem", opacity: 0.75 }}><code>i2v</code>: Animate an image and use multiple keyframes</p>
    </div>

    <div>
      <MutedVideo src="https://cdn.sanity.io/files/2gpum2i6/production/08c30d095b03c80ebec84e8a547c3ea694c2efca.mp4" alt="Video continuation example" />

      <p style={{ margin: "0.6rem 0 0", fontSize: "0.9rem", opacity: 0.75 }}><code>v2v</code>: Extend your clip</p>
    </div>
  </div>

  ## Parameters

  Full schema and per-field constraints live in the [API reference](/api-reference/utility/generate-a-video-with-flux-3).

  | Field | Required | |
  | - | - | - |
  | `mode` | always | `t2v`, `i2v`, `v2v`, or `draft_enhance`. |
  | `prompt` | always | What to generate. |
  | `keyframes` | for `i2v` | One image starts the clip, two pin start and end, `[seconds, image]` pairs pin exact times. URL or base64. |
  | `start_video` | for `v2v` | The clip to continue from, mp4 as URL or base64. |
  | `draft_cache` | for `draft_enhance` | The bundle from a prior draft; reproduces that generation at full quality. |
  | `resolution` | | `hd` (default), `fhd`, `qhd`, or `uhd`. See [Resolution](#resolution). |
  | `duration` | | Whole seconds, 5 to 20, or `auto` (default). |
  | `aspect_ratio` | | `auto` (default), `21:9`, `2:1`, `16:9`, `4:3`, `1:1`, `3:4`, `9:16`, or `9:21`. |
  | `generate_audio` | | On by default. Set `false` for a silent clip. |
  | `safety_tolerance` | | 0 (strictest) to 4, default 2. |
  | `draft` | | Set `true` for a fast hd preview; the result includes a `draft_cache` bundle. |
  | `version` | | `latest` (default): always tracks the current release. |

  ### Resolution

  `hd` is delivered straight off the generation. `fhd`, `qhd`, and `uhd` run the
  same generation and finish it with the video upsampler on a larger canvas, so
  the shot is identical across all four and only the delivered size differs.

  | Aspect ratio | `hd` | `fhd` | `qhd` | `uhd` |
  | - | - | - | - | - |
  | 21:9 | 1440 × 608 | 2176 × 928 | 2912 × 1248 | 4352 × 1856 |
  | 2:1 | 1344 × 672 | 2016 × 1024 | 2720 × 1376 | 4032 × 2048 |
  | 16:9 | 1280 × 704 | 1920 × 1088 | 2560 × 1440 | 3840 × 2176 |
  | 4:3 | 1088 × 800 | 1632 × 1216 | 2176 × 1632 | 3264 × 2432 |
  | 1:1 | 960 × 960 | 1440 × 1440 | 1920 × 1920 | 2880 × 2880 |
  | 3:4 | 800 × 1088 | 1216 × 1632 | 1632 × 2176 | 2432 × 3264 |
  | 9:16 | 704 × 1280 | 1088 × 1920 | 1440 × 2560 | 2176 × 3840 |
  | 9:21 | 608 × 1440 | 928 × 2176 | 1248 × 2912 | 1856 × 4352 |

  Dimensions are multiples of 32, so a class does not always land on the round
  number its name suggests: `uhd` at 16:9 delivers 3840 × 2176 rather than
  3840 × 2160, the same way `fhd` delivers 1920 × 1088. Crop the delivered frame
  if you need an exact standard size.

  Drafts always render at `hd`: sending `draft: true` with any other resolution
  is rejected. `draft_enhance` accepts all four classes and defaults to `fhd`, so
  the delivery size is chosen when you commit a draft, not when you preview it.

  ## Use cases

  Deeply customizable, and built for use cases, visual styles, and aspect ratios
  far beyond conventional cinematic output.

  ### Text to video

  Generate a clip from words alone: FLUX 3 handles everything from a single line to a densely directed brief, filling in framing, motion, and mood wherever you leave them open.

  <div className="flux3-usecase-slides">
    <ClipSlider
      items={[
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/fc743c980a0259403ccec6c9a705f9e412e6f81b.mp4", poster: "https://cdn.sanity.io/images/2gpum2i6/production/96a96727d53a1d45c9854c8ef9ef67858361871a-1280x704.jpg", title: "Example 1", prompt: "an amateur recording of a night time walk through a forest, harsh white light from a headlamp, in the forest they find a very large unused gothic church, no windows, reclaimed by nature" },
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/f9ecaee9534b92e0dbbf8b01e47cec0b1ebfb513.mp4", title: "Example 2", prompt: "A continuous helmet-mounted POV tails a woman on a dirt bike racing across rolling desert dunes, her tracks the only marks on the wind-sculpted sand. The view dips and jolts over each crest as she kicks up golden arcs into the low, raking sun. The engine snarls and roars, the only sound in the sunlit void." },
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/54b9d5d4b47ee20eb7650099b2e27f353eb1acc5.mp4", poster: "https://cdn.sanity.io/images/2gpum2i6/production/9432cfd9aa0b32e403f67897e535f4868ea66c10-1280x704.jpg", title: "Example 3", prompt: "A split-screen view. It shows a living room. On the left side, the living room is filmed from above, looking down from the ceiling, and the right side shows a camera sitting on a shelf. A cat is sitting on another shelf, jumps across the room, and then lands on the camera. When it lands on the camera, the right side is obscured and shows a close-up of the cat. The left and right sides are completely synchronized." },
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/cbb9548e1a2d9f7b8d8d22644bef6489425aad32.mp4", poster: "https://cdn.sanity.io/images/2gpum2i6/production/9a8414f02355d61231b7999decb2be143a746ff1-1280x704.jpg", title: "Example 4" },
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/77b2fbf640b456ff93b2eca5bf013b7e1145ea5f.mp4", poster: "https://cdn.sanity.io/images/2gpum2i6/production/d0cadba3ca86e84c1e6d86de87555a20f522f62c-1280x704.jpg", title: "Example 5", prompt: "Dashcam view of a moose crossing a snowy highway at dusk, wipers sweeping, brake lights reflecting on ice." },
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/5543d62e19e4bcc3b23d356a1065ff90f15552dd.mp4", poster: "https://cdn.sanity.io/images/2gpum2i6/production/45474bc809f92e1b9a392d8ec209a857c4e2b24d-1280x704.png", title: "Example 6", prompt: "ONE continuous unbroken real-time shot, constant motion, no freezing. A DOLLY ZOOM, the classic vertigo effect, executed precisely: a man in a long dark coat stands with his back to us at the stone parapet of a rooftop garden, dead center frame, and he stays EXACTLY the same size in frame for the entire shot while the camera physically pulls backward and the lens zooms in: so the city skyline beyond him compresses, looms, and swells unnaturally toward us, towers flattening against each other, perspective warping around his motionless silhouette. And as the background compresses, the reveal: between the looming towers there are no streets: only open green ocean water, waves moving where avenues should be, ship wakes crossing between the buildings. He never moves. Ivy trembles on the parapet, his coat stirs in the wind. Muted twilight grade, sodium lights waking in the towers. Audio: a spiraling string figure that climbs and never resolves, wind at altitude, and beneath it: impossibly: the slow roll of surf echoing up between the buildings; no voices. No on-screen text." },
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/8d214be331058ca6b63506dfdbe713bde0a58b77.mp4", poster: "https://cdn.sanity.io/images/2gpum2i6/production/ceae0e535b13ed1e6753ec2cef2555f7d8542c4d-1280x704.png", title: "Example 7", prompt: "1990s hand-drawn cel animation, original magical-transformation sequence resembling no existing show: sparkling painted backgrounds, ribbon light effects, bold ink lines, high-energy rotation: constant motion from the first frame. An ordinary EGG on a kitchen counter at dawn is chosen: it trembles, lifts off the counter into a swirling column of golden light, and undergoes a full dramatic magical-girl-style TRANSFORMATION SEQUENCE: spinning against a starry void, shell cracking in radiant petals, ribbons of yolk-light spiraling around it, flash-cuts of its silhouette evolving: into its final form: a magnificent gleaming FRIED EGG, sunny side up, wearing a tiny cape of crisped lace-edge, holding a heroic pose atop a slice of toast that rises beneath it like a pedestal, backlit by a radiant painted sunrise, butter-sparkles raining. The camera orbits the transformation in accelerating anime rotation, snap-zooms on the cracking shell, holds the final hero pose with wind machine drama as the cape's lacy edge flutters. The kitchen returns to normal around it; a fork and knife lie crossed before the toast pedestal like offered swords. No people anywhere. Audio: a full transformation jingle: harp glissandi, choir swell, synth fanfare: the shell's crystal cracks, the sizzle blooming exactly as the yolk-light condenses, the final TA-DAA chord, then quiet kitchen morning with one small heroic sizzle. No on-screen text, no logos, no subtitles, no credits." },
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/21143c68277202cd96809af03e2c536b9424c8e9.mp4", poster: "https://cdn.sanity.io/images/2gpum2i6/production/2279faa8dd903dd3ef91f16913fde8f616aafd42-1440x608.png", title: "Example 8", prompt: "Thermal infrared wildlife cinematography, false-color heat palette: a savanna at absolute night rendered in blooming whites and deep blues: a pride of lions moving as white-hot ghosts through cold indigo grass, their breath curling as bright plumes, a heat-shimmer mirage where the day's warmth still rises off a rock, distant elephants as slow warm mountains, one lioness pausing to look toward the camera, her eyes flaring as the hottest points in frame. The palette shifts subtly as a cold wind passes through the grass. Audio: night insects, a distant lion contact call resonating, the wind, no music. No on-screen text." },
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/40e995b48d5af914a99f053d207b91cbf0bd516b.mp4", poster: "https://cdn.sanity.io/images/2gpum2i6/production/4723e8a3bab8fcc8bae9c2e27e164c08e2115e72-1280x704.png", title: "Example 9", prompt: "A beekeeper lifts the lid off a hive in the last sunlight, and the rising bees become hundreds of golden sparks spiraling upward into the light. Phase 1 (0–3s): 85mm behind the hive into the low sun: the beekeeper's veiled silhouette rimmed in gold, the hum inside the box deepening as gloved hands grip the lid, smoke from the smoker drifting flat and luminous. Phase 2 (3–7s): The lid lifts: the hum blooms: and bees rise in a loose column, each one a burning point against the dark treeline, spiraling and weaving through the smoke like slow sparks from a fire. Phase 3 (7–10s): The beekeeper stands motionless in the glittering cloud, deadpan, holding the lid like a shield of light; the camera pushes in gently as the column bends toward the sun and the hum settles to a contented evening drone." },
]}
    />
  </div>

  <Accordion title="Example API call">
    ```bash cURL theme={null}
    curl -s -X POST https://api.bfl.ai/v1/flux-3-video \
      -H "x-key: $BFL_API_KEY" -H "Content-Type: application/json" \
      -d '{
        "mode": "t2v",
        "prompt": "a fox running through dawn mist",
        "generate_audio": true
      }'
    ```
  </Accordion>

  ### Image to video

  Animate from images you pin. A single still becomes the exact opening frame, a start-and-end pair interpolates the motion between them, and several frames spread across the clip.

  <div className="flux3-usecase-slides">
    <AnimateSlider
      items={[
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/3bf3b94bfd068396e9b7b7c73421c5316fbc3048.mp4", image: "https://cdn.sanity.io/images/2gpum2i6/production/b015fdbe652ebfac7e834067fe9091d670f35cff-1280x736.png" },
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/9b7469e7f349b4f26fe9809b9faa13567504aa84.mp4", image: "https://cdn.sanity.io/images/2gpum2i6/production/a121750cb4422945a6d3ff0eed246271750e3c63-1280x704.jpg" },
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/ce0f3c021525c568ad591daf681f3d9c601f21aa.mp4", image: "https://cdn.sanity.io/images/2gpum2i6/production/58430b2254241f059ce59ec01ed4b11ad9fce95e-1280x704.jpg" },
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/b993a73e27a11f6ba0e64f25616cb0786685085c.mp4", image: "https://cdn.sanity.io/images/2gpum2i6/production/2a418d0b787e96624ade85707bff5100e9742838-1280x736.jpg" },
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/6bc0f335c3638949f9df86ca1d3c8b273eac8e25.mp4", image: "https://cdn.sanity.io/images/2gpum2i6/production/a9427d24b941e1cf9494b18387b06f2a5439669d-1280x704.jpg" },
]}
    />
  </div>

  <Accordion title="Example API call">
    ```bash cURL theme={null}
    curl -s -X POST https://api.bfl.ai/v1/flux-3-video \
      -H "x-key: $BFL_API_KEY" -H "Content-Type: application/json" \
      -d '{
        "mode": "i2v",
        "prompt": "push forward through the trees",
        "keyframes": "https://example.com/still.jpg"
      }'
    ```
  </Accordion>

  ### Multiple scenes

  Block several shots and camera angles in one generation. Character, look, and continuity hold across hard cuts, with a single audio bed carrying through.

  <div className="flux3-usecase-slides">
    <ClipSlider
      items={[
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/24be70f578f03010f5fe5426e57c65a6e5921706.mp4", poster: "https://cdn.sanity.io/images/2gpum2i6/production/f4227eacc451c012bea71ff4a4c73dbd2b74d6b1-1280x704.jpg" },
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/b99a94ed131a2dcc95066a3d347014d7eea2d063.mp4", poster: "https://cdn.sanity.io/images/2gpum2i6/production/1a5705842db80e336cc914f640515a02e735f5cb-1280x704.jpg" },
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/e275b9bb21f8c0b09805ee8e008ef8c06aa7162b.mp4", poster: "https://cdn.sanity.io/images/2gpum2i6/production/631fc883f4b5a29dc8e9cb7197f5525ffd8043a6-1280x704.jpg" },
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/82ae19ea0ca3a328a490f5514417f6724bfbf416.mp4", poster: "https://cdn.sanity.io/images/2gpum2i6/production/17f424b1fd8fd93d6a09d24c53aa0f5d7afce7c7-1280x704.jpg" },
]}
    />
  </div>

  <Accordion title="Example API call">
    ```bash cURL theme={null}
    curl -s -X POST https://api.bfl.ai/v1/flux-3-video \
      -H "x-key: $BFL_API_KEY" -H "Content-Type: application/json" \
      -d '{
        "mode": "t2v",
        "prompt": "SHOT ONE: wide aerial of a desert highway at dawn. HARD CUT. SHOT TWO: interior close-up of the driver. HARD CUT. SHOT THREE: the car shrinks into the heat haze. One music bed across all shots.",
        "duration": 10
      }'
    ```
  </Accordion>

  ### Multilingual dialogue

  On-camera speech in many languages, with accurate accents and tight lipsync. Quote the line in your prompt and the character says it.

  <div className="flux3-usecase-slides">
    <ClipSlider
      items={[
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/10b82418b5cb41003dfed28f36b000f5d2a859d4.mp4", poster: "https://cdn.sanity.io/images/2gpum2i6/production/f7b40cc69d9264e534aababdcdf789608de9552b-1280x704.png" },
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/87193f21069e572ba8082488ae8123b9e9c0ddbd.mp4", poster: "https://cdn.sanity.io/images/2gpum2i6/production/7baa8fdf0b8280a8ee7a9ffe80283e69f52387aa-1280x704.jpg" },
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/27c782504418881e54d7008e6ad8f1ec73590b8c.mp4", poster: "https://cdn.sanity.io/images/2gpum2i6/production/0fc6da2a8639ed60bb706398434c1fdd12d658ad-1280x704.jpg" },
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/ad9f876b4a385c7be2d7639b63933f30e64a25f0.mp4", poster: "https://cdn.sanity.io/images/2gpum2i6/production/a3dc6c56a03ed9a26964734515a1d8d19f07f8f2-1280x704.jpg" },
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/e9bc8a59903d4912825410441bb3c16a23d07e9f.mp4", poster: "https://cdn.sanity.io/images/2gpum2i6/production/a35a1bff78edb3e6ab18acf68c9c5b9da8eba04a-1280x704.jpg" },
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/ad885036a38d8f1c47008719a13322a52ca68299.mp4", poster: "https://cdn.sanity.io/images/2gpum2i6/production/2f1cd533fdd713d52cba8197fbf6f47be50184a2-1280x704.png", prompt: "Overwrought 1990s telenovela scene, soft studio lighting, slightly warm video look: in a lavish hacienda living room, an elegant woman in red confronts a mustached man in a white suit: she gasps and turns to camera as ANOTHER identical mustached man in an identical white suit enters through the double doors, the camera CRASH-ZOOMS to her shocked face, then whip-pans between the two identical men, each raising one eyebrow in perfect sync, a dramatic organ sting on each zoom, her hand rising slowly to her mouth, a single tear. One of the twins narrows his eyes. Audio: melodramatic Spanish dialogue: her gasped accusation, the twins answering in unison: thunderous organ stings on every zoom, a swelling string section. No on-screen text." },
]}
    />
  </div>

  <Accordion title="Example API call">
    ```bash cURL theme={null}
    curl -s -X POST https://api.bfl.ai/v1/flux-3-video \
      -H "x-key: $BFL_API_KEY" -H "Content-Type: application/json" \
      -d '{
        "mode": "t2v",
        "prompt": "A presenter speaks to the lens: \"Storm season is here.\" Clean studio lighting.",
        "generate_audio": true
      }'
    ```
  </Accordion>

  ### Broad range of styles

  Specify the visual style in the prompt: live action, animation, or motion graphics.

  <div className="flux3-usecase-slides">
    <ClipSlider
      items={[
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/8f211de4af11cc268406edf8c61d07a88487c5cb.mp4", poster: "https://cdn.sanity.io/images/2gpum2i6/production/6e063f229f1282549f209fe7f248ec6f0fe037e6-1280x704.jpg" },
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/aaf25c1cee9bec53385a281eecd983e60efad549.mp4", poster: "https://cdn.sanity.io/images/2gpum2i6/production/d79995d2a2db1c4843d32bcf6084a85fe1ea641f-1280x704.jpg" },
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/5bcf3feb0b6b8041bd9666648d3f234dc862133c.mp4", poster: "https://cdn.sanity.io/images/2gpum2i6/production/df4a7b56a41e9dba0b96c6a04ddcbbc969ad2855-1280x704.jpg" },
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/13bd9565c5cc73d8d447ca5c531d69fe6a14dc87.mp4", poster: "https://cdn.sanity.io/images/2gpum2i6/production/26b46a1f8e6e529aaa00060bf0c0163d8f77e55f-1280x704.jpg" },
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/5e8a2414bbd8a1047b5f12866007851f226692e9.mp4", poster: "https://cdn.sanity.io/images/2gpum2i6/production/0d72a611f4feda1a2b859600eeb251fdea953a58-1280x704.jpg" },
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/5729a757e0c75e447e07e8d9d81890ee1497e60d.mp4", poster: "https://cdn.sanity.io/images/2gpum2i6/production/0cd21bacc2c6bbfccab274ec0c01a1d3c583c899-1280x704.jpg" },
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/b2d51a02a819316f46e988b2e2f4a5cb22425dd8.mp4", poster: "https://cdn.sanity.io/images/2gpum2i6/production/409aeb06da187764f5e7d57bacd2a606a0266cde-1280x704.png", prompt: "ONE continuous shot, constant motion, black studio void: two dancers made ENTIRELY of wet paint: one cobalt blue, one cadmium orange: perform a violent, gorgeous duet: every spin flings arcing ribbons of themselves that hang in the air, every clash splashes both colors into short-lived green-brown blooms where they mix, limbs re-forming from the airborne paint as they pull it back into their bodies, a lift where the blue dancer pours upward through the orange one's arms and reconstitutes above. The floor accumulates their history as a growing action painting. High-speed clarity on every droplet. Audio: percussive modern dance score, wet impacts, the hiss of paint slicing air. No on-screen text." },
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/3471a6e0c6bdcf5d8cd27d76eee5412d832ab373.mp4", poster: "https://cdn.sanity.io/images/2gpum2i6/production/82648cc4565f160cfce5c40eb3af7d055648f584-1280x704.png", prompt: "Retro 1980s cel anime, hand-painted backgrounds, flat cel shading, visible film grain and slight gate weave, painted rain: a deserted night-market alley under paper lanterns in heavy rain, awnings dripping, neon reflections painted in the puddles. On a wooden counter a huge steaming bowl of ramen sits alone. The noodles begin to rise from the broth and coil upward into a long serpentine dragon made entirely of noodles, chopstick-thin whiskers, steam pouring off it as wings, egg halves for eyes; it coils once around the lantern above the stall, scattering painted raindrops, then dives back into the bowl with a splash of broth, the surface settling as if nothing happened. The camera whip-tilts up from the bowl to follow the rise, holds on the coil around the lantern, then whip-tilts down for the dive. Everything moves in cel-animation language: held frames, smears on the fast moves, painted splash shapes. This is rendered anime cel animation only, NOT live action, NOT photorealistic film. Audio: rain on canvas awnings, a low synth score sting as the dragon rises, a wet slurping whoosh on the dive, lantern creak. No people anywhere in the entire video, no human hands, no human voices. No on-screen text, no logos, no subtitles, no credits." },
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/95a79d0f1046a3b74aecd78c94ccdcba8d5bdfc5.mp4", poster: "https://cdn.sanity.io/images/2gpum2i6/production/fe6d57fb1ddf8f82d96d32fe7184bb6074dc98b6-1280x704.png" },
]}
    />
  </div>

  <Accordion title="Example API call">
    ```bash cURL theme={null}
    curl -s -X POST https://api.bfl.ai/v1/flux-3-video \
      -H "x-key: $BFL_API_KEY" -H "Content-Type: application/json" \
      -d '{
        "mode": "t2v",
        "prompt": "2D hand-drawn animation of a fox leaping through a paper-cut forest, bold flat colors",
        "aspect_ratio": "16:9"
      }'
    ```
  </Accordion>

  ### Text and typography

  Describe the words and their placement to generate titles, signs, or lower-thirds within the video.

  <div className="flux3-usecase-slides">
    <ClipSlider
      items={[
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/123e67046dd3f79f72f548c0b28032298935861f.mp4", poster: "https://cdn.sanity.io/images/2gpum2i6/production/8cb4ad88bf80f0a94d5150019fb8e1dc12adba6a-1280x704.jpg" },
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/d4cf506cb619726f87edc91b80d59cfbf35c6c24.mp4", poster: "https://cdn.sanity.io/images/2gpum2i6/production/d74a6cbe510075724fec3449d173eb0250c75d4a-1280x704.jpg" },
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/25419bd396d4efabca1295c6dbf6dedd27372022.mp4", poster: "https://cdn.sanity.io/images/2gpum2i6/production/fc5777f2e9b8bb14a975ac8c888a740cf7e4317f-1280x704.jpg" },
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/f022402b3fb1e9105edfb0cffb44512e9495aa9b.mp4", poster: "https://cdn.sanity.io/images/2gpum2i6/production/50a2377cbb5d5f7014cd6e1f1033955e01b87285-1280x704.jpg" },
]}
    />
  </div>

  <Accordion title="Example API call">
    ```bash cURL theme={null}
    curl -s -X POST https://api.bfl.ai/v1/flux-3-video \
      -H "x-key: $BFL_API_KEY" -H "Content-Type: application/json" \
      -d '{
        "mode": "t2v",
        "prompt": "Bold kinetic title card, the word \"FLUX\" assembling from light streaks on a dark stage",
        "aspect_ratio": "16:9"
      }'
    ```
  </Accordion>

  ### Keyframes

  Provide ordered keyframes to guide a continuous shot through specific poses and compositions. FLUX 3 generates the motion between them.

  <Tabs>
    <Tab title="Example 1">
      <Columns cols={4}>
        <div>
          <img src="https://cdn.sanity.io/images/2gpum2i6/production/809c8dc93242314493c4ae613e9ebfba26b7b14f-2752x1536.jpg" alt="Keyframe: Start (0:00)" style={{ width: "100%", display: "block", borderRadius: "0.5rem" }} />

          <p style={{ textAlign: "center", marginTop: "0.4rem", opacity: 0.7, fontSize: "0.85rem" }}>Start (0:00)</p>
        </div>

        <div>
          <img src="https://cdn.sanity.io/images/2gpum2i6/production/716f9cad904a337d7d54cbe3fa69d8cc8fb6fde5-2752x1536.jpg" alt="Keyframe: Key 2 (0:03)" style={{ width: "100%", display: "block", borderRadius: "0.5rem" }} />

          <p style={{ textAlign: "center", marginTop: "0.4rem", opacity: 0.7, fontSize: "0.85rem" }}>Key 2 (0:03)</p>
        </div>

        <div>
          <img src="https://cdn.sanity.io/images/2gpum2i6/production/b4855f2bcf674551255248f4f30a2f213ed34e68-2752x1536.jpg" alt="Keyframe: Key 3 (0:07)" style={{ width: "100%", display: "block", borderRadius: "0.5rem" }} />

          <p style={{ textAlign: "center", marginTop: "0.4rem", opacity: 0.7, fontSize: "0.85rem" }}>Key 3 (0:07)</p>
        </div>

        <div>
          <img src="https://cdn.sanity.io/images/2gpum2i6/production/b591a58d3a4a94472dbb7e69f3f40859cc6d3731-2752x1536.jpg" alt="Keyframe: End (0:10)" style={{ width: "100%", display: "block", borderRadius: "0.5rem" }} />

          <p style={{ textAlign: "center", marginTop: "0.4rem", opacity: 0.7, fontSize: "0.85rem" }}>End (0:10)</p>
        </div>
      </Columns>

      <MutedVideo src="https://cdn.sanity.io/files/2gpum2i6/production/afc86a790f1a835db3cff62e58dd658c3d47fc59.mp4" alt="Keyframes interpolation: example 1" />
    </Tab>

    <Tab title="Example 2">
      <Columns cols={4}>
        <div>
          <img src="https://cdn.sanity.io/images/2gpum2i6/production/358bebb751c3867f0901e30124f3883e76ab7bbb-2752x1536.jpg" alt="Keyframe: Start" style={{ width: "100%", display: "block", borderRadius: "0.5rem" }} />

          <p style={{ textAlign: "center", marginTop: "0.4rem", opacity: 0.7, fontSize: "0.85rem" }}>Start</p>
        </div>

        <div>
          <img src="https://cdn.sanity.io/images/2gpum2i6/production/54cea0c9d4713e182e6efca23af4aaded1692eeb-1280x704.jpg" alt="Keyframe: Key 2" style={{ width: "100%", display: "block", borderRadius: "0.5rem" }} />

          <p style={{ textAlign: "center", marginTop: "0.4rem", opacity: 0.7, fontSize: "0.85rem" }}>Key 2</p>
        </div>

        <div>
          <img src="https://cdn.sanity.io/images/2gpum2i6/production/7bae0d042e4439dc663ef4c9a9e742a0ea7d7ba2-2752x1536.jpg" alt="Keyframe: Key 3" style={{ width: "100%", display: "block", borderRadius: "0.5rem" }} />

          <p style={{ textAlign: "center", marginTop: "0.4rem", opacity: 0.7, fontSize: "0.85rem" }}>Key 3</p>
        </div>

        <div>
          <img src="https://cdn.sanity.io/images/2gpum2i6/production/5e23500b2d474706664f50d049b773881bdf08df-2752x1536.jpg" alt="Keyframe: End" style={{ width: "100%", display: "block", borderRadius: "0.5rem" }} />

          <p style={{ textAlign: "center", marginTop: "0.4rem", opacity: 0.7, fontSize: "0.85rem" }}>End</p>
        </div>
      </Columns>

      <MutedVideo src="https://cdn.sanity.io/files/2gpum2i6/production/3f5235ad7fc4479887c52b832e0b53162443192a.mp4" alt="Keyframes interpolation: example 2" />
    </Tab>

    <Tab title="Example 3">
      <Columns cols={4}>
        <div>
          <img src="https://cdn.sanity.io/images/2gpum2i6/production/8ceb96c14c79c734374de4eca3ee56ba6c122cb9-2752x1536.jpg" alt="Keyframe: Start (0:00)" style={{ width: "100%", display: "block", borderRadius: "0.5rem" }} />

          <p style={{ textAlign: "center", marginTop: "0.4rem", opacity: 0.7, fontSize: "0.85rem" }}>Start (0:00)</p>
        </div>

        <div>
          <img src="https://cdn.sanity.io/images/2gpum2i6/production/46eba9ae12d22d874b38bbdb4912d8b5ce0ae174-2752x1536.jpg" alt="Keyframe: Key 2 (0:03)" style={{ width: "100%", display: "block", borderRadius: "0.5rem" }} />

          <p style={{ textAlign: "center", marginTop: "0.4rem", opacity: 0.7, fontSize: "0.85rem" }}>Key 2 (0:03)</p>
        </div>

        <div>
          <img src="https://cdn.sanity.io/images/2gpum2i6/production/e3dfbe33f753a1ce8b3cae684dd4b6a623a41508-2752x1536.jpg" alt="Keyframe: Key 3 (0:07)" style={{ width: "100%", display: "block", borderRadius: "0.5rem" }} />

          <p style={{ textAlign: "center", marginTop: "0.4rem", opacity: 0.7, fontSize: "0.85rem" }}>Key 3 (0:07)</p>
        </div>

        <div>
          <img src="https://cdn.sanity.io/images/2gpum2i6/production/49e52bc3ba7be3391053bd0a44b65312955acbc6-2752x1536.jpg" alt="Keyframe: End (0:10)" style={{ width: "100%", display: "block", borderRadius: "0.5rem" }} />

          <p style={{ textAlign: "center", marginTop: "0.4rem", opacity: 0.7, fontSize: "0.85rem" }}>End (0:10)</p>
        </div>
      </Columns>

      <MutedVideo src="https://cdn.sanity.io/files/2gpum2i6/production/44b47fe10cbc6e12254a62e551039f6fdbe0344c.mp4" alt="Keyframes interpolation: example 3" />
    </Tab>

    <Tab title="Example 4">
      <Columns cols={4}>
        <div>
          <img src="https://cdn.sanity.io/images/2gpum2i6/production/a0250de4e82156784a473cbc3d80962eec182f67-2752x1536.jpg" alt="Keyframe: Start (0:00)" style={{ width: "100%", display: "block", borderRadius: "0.5rem" }} />

          <p style={{ textAlign: "center", marginTop: "0.4rem", opacity: 0.7, fontSize: "0.85rem" }}>Start (0:00)</p>
        </div>

        <div>
          <img src="https://cdn.sanity.io/images/2gpum2i6/production/0fb824a0f323555e7bf638ba15297560929f5910-2752x1536.jpg" alt="Keyframe: Key 2 (0:03)" style={{ width: "100%", display: "block", borderRadius: "0.5rem" }} />

          <p style={{ textAlign: "center", marginTop: "0.4rem", opacity: 0.7, fontSize: "0.85rem" }}>Key 2 (0:03)</p>
        </div>

        <div>
          <img src="https://cdn.sanity.io/images/2gpum2i6/production/b53d1ad12728e294e24ac379f9911fa6d23394df-2752x1536.jpg" alt="Keyframe: Key 3 (0:07)" style={{ width: "100%", display: "block", borderRadius: "0.5rem" }} />

          <p style={{ textAlign: "center", marginTop: "0.4rem", opacity: 0.7, fontSize: "0.85rem" }}>Key 3 (0:07)</p>
        </div>

        <div>
          <img src="https://cdn.sanity.io/images/2gpum2i6/production/5f041547138189029876ea4e578ab1f563fad5f4-2752x1536.jpg" alt="Keyframe: End (0:10)" style={{ width: "100%", display: "block", borderRadius: "0.5rem" }} />

          <p style={{ textAlign: "center", marginTop: "0.4rem", opacity: 0.7, fontSize: "0.85rem" }}>End (0:10)</p>
        </div>
      </Columns>

      <MutedVideo src="https://cdn.sanity.io/files/2gpum2i6/production/f5b698d51ec802449f9facb5d57ab62b5dd1c68c.mp4" alt="Keyframes interpolation: example 4" />
    </Tab>
  </Tabs>

  <Accordion title="Example API call">
    ```bash cURL theme={null}
    curl -s -X POST https://api.bfl.ai/v1/flux-3-video \
      -H "x-key: $BFL_API_KEY" -H "Content-Type: application/json" \
      -d '{
        "mode": "i2v",
        "prompt": "a seed grows into a tree through the seasons",
        "duration": 10,
        "keyframes": [[0, "https://example.com/seed.png"], [4.5, "https://example.com/sapling.png"], [10, "https://example.com/tree.png"]]
      }'
    ```
  </Accordion>

  ### Video continuation

  Feed an existing clip and FLUX 3 picks up from its final frames, carrying momentum, framing, and scene logic forward without a cut.

  <Tabs>
    <Tab title="Elephants">
      <VideoExtension src="https://cdn.sanity.io/files/2gpum2i6/production/08c30d095b03c80ebec84e8a547c3ea694c2efca.mp4" poster="https://cdn.sanity.io/images/2gpum2i6/production/2b51df2fc7e43550f57b1cb72fe64b6122197de7-1280x704.jpg" originalDuration={5} alt="Elephants: video continuation" />
    </Tab>

    <Tab title="Penguin">
      <VideoExtension src="https://cdn.sanity.io/files/2gpum2i6/production/c3251e7b67573784027544e16752c517eb73f7b7.mp4" poster="https://cdn.sanity.io/images/2gpum2i6/production/43a63bebd9601c5748797cd2aa6e71785dbf8b23-1280x704.jpg" originalDuration={5} alt="Penguin: video continuation" />
    </Tab>

    <Tab title="Iguazú Falls">
      <VideoExtension src="https://cdn.sanity.io/files/2gpum2i6/production/69c7cd8f26d64630880b5e330581cb2312f0645c.mp4" poster="https://cdn.sanity.io/images/2gpum2i6/production/ab596e488b835529ccb40b0d3ba26be1ae8b9dd6-1280x704.jpg" originalDuration={5} alt="Iguazú Falls: video continuation" />
    </Tab>

    <Tab title="Giraffe">
      <VideoExtension src="https://cdn.sanity.io/files/2gpum2i6/production/d1368e9ad2193e8e85b0165b6fa793e4920546f1.mp4" poster="https://cdn.sanity.io/images/2gpum2i6/production/42c4ae3bab1aca4a3f86e387526daf501c635d64-1280x704.jpg" originalDuration={5} alt="Giraffe: video continuation" />
    </Tab>

    <Tab title="SUV drift">
      <VideoExtension src="https://cdn.sanity.io/files/2gpum2i6/production/8f587923157cc33e6558fdd4d6c0a96ff6a6fdc8.mp4" poster="https://cdn.sanity.io/images/2gpum2i6/production/a1de57d82c1f75869133b4557b81faf3a935f75c-1280x704.jpg" originalDuration={5} alt="SUV drift: video continuation" />
    </Tab>
  </Tabs>

  <Accordion title="Example API call">
    ```bash cURL theme={null}
    curl -s -X POST https://api.bfl.ai/v1/flux-3-video \
      -H "x-key: $BFL_API_KEY" -H "Content-Type: application/json" \
      -d '{
        "mode": "v2v",
        "prompt": "she takes his hand and pulls him laughing through the lantern-lit alley, the camera chasing them",
        "start_video": "https://example.com/clip.mp4",
        "duration": 10
      }'
    ```
  </Accordion>

  ### Video editing

  The example below requests two changes to an existing clip: remove the
  pedestrians and add pigeons around the fountain.

  <VideoComparisonSlider beforeVideo="https://cdn.sanity.io/files/2gpum2i6/production/a38495a2841be9a8d2827d48df5f17aec7b5ac98.mp4" afterVideo="https://cdn.sanity.io/files/2gpum2i6/production/5b163e044b56fb0db536693a4aa97e737ddfbad3.mp4" beforeLabel="Source" afterLabel="Edited" />

  <PromptDisplay prompt="Remove all the pedestrians and add a flock of pigeons around the fountain." />

  FLUX Video Edit \[fast] takes the source clip as `video` and the edit instruction as
  `prompt` at `POST /v1/flux-tools/video-edit-v1`. Poll the returned `polling_url`
  until `Ready` and download `result.sample`. Twelve edits on one clip, the
  request fields, limits and pricing are on the
  [FLUX Video Edit page](/flux_tools/flux_video_edit).

  ## Keep exploring

  <CardGroup cols={2}>
    <Card title="Prompting Guide" icon="pen" href="/guides/prompting_video_text_to_video">
      Prompt formats, the schema, camera language, and audio.
    </Card>

    <Card title="API reference" icon="code" href="/api-reference/utility/generate-a-video-with-flux-3">
      The full flux-3-video request contract: modes, fields, and constraints.
    </Card>

    <Card title="Playground" icon="play" href="https://playground.bfl.ai">
      Try FLUX 3 in your browser: no setup required.
    </Card>

    <Card title="FLUX 3 overview" icon="film" href="/flux_3/flux3_overview">
      One request shape across image, video, and synchronized audio.
    </Card>
  </CardGroup>
</div>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.