> ## 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. Up to full-HD and 20 seconds, with synchronized audio.

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 obsWiredRef = 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`);
  };
  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 attachNav = el => {
    navRef.current = el;
    if (!el || obsWiredRef.current) return;
    if (typeof IntersectionObserver === "undefined") return;
    obsWiredRef.current = true;
    const observer = new IntersectionObserver(entries => {
      entries.forEach(entry => {
        if (entry.isIntersecting && entry.target.id) {
          const href = `#${entry.target.id}`;
          setActive(href);
          centerPill(href);
        }
      });
    }, {
      rootMargin: "-12% 0px -78% 0px",
      threshold: 0
    });
    SECTIONS.forEach(s => {
      const node = document.getElementById(s.href.slice(1));
      if (node) observer.observe(node);
      (s.children || []).forEach(c => {
        const cn = document.getElementById(c.href.slice(1));
        if (cn) observer.observe(cn);
      });
    });
  };
  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={`not-prose camera-nav${pinned ? " is-pinned" : ""}${subItems.length ? " has-sub" : ""}`} 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={`camera-nav__pill${isParentActive(s) ? " is-active" : ""}`} aria-current={isParentActive(s) ? "true" : undefined} onClick={() => 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={`camera-nav__pill camera-nav__pill--sub${active === c.href ? " is-active" : ""}`} aria-current={active === c.href ? "true" : undefined} onClick={() => 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 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="clip-slider__media">
              <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>}
    </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>;
};

<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: "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

  One `mode` per request; the rest of the request stays the same.

  * **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.

  <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) or `fhd`.                                                                                   |
  | `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`, or `9:16`.                                   |
  | `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.                                                    |

  ## 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", prompt: "Prompt: …" },
{ 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: "Prompt: …" },
{ 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: "Prompt: …" },
{ 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: "Prompt: …" },
{ 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" },
]}
    />
  </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

  Photoreal cinematography, animation, motion design, and stylized looks — the same model spans styles far beyond conventional footage.

  <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" },
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/3471a6e0c6bdcf5d8cd27d76eee5412d832ab373.mp4", poster: "https://cdn.sanity.io/images/2gpum2i6/production/82648cc4565f160cfce5c40eb3af7d055648f584-1280x704.png" },
{ 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

  Legible, well-placed text rendered as part of the scene — titles, signage, and lower-thirds that stay stable through motion.

  <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

  Pin ordered keyframes and FLUX 3 interpolates one continuous shot through each — hit this pose, then this composition, then this — while the model handles 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>

  ## 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>
