> ## 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 Generation with FLUX

> Learn how to prompt FLUX 3 for text-to-video, image-to-video, clip extension, and video editing workflows

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 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>;
};

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 LoopClip = ({src, poster, borderRadius = "0.75rem"}) => {
  const videoRef = useRef(null);
  const [muted, setMuted] = useState(true);
  const toggleMute = e => {
    e.stopPropagation();
    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%",
    marginBottom: "1rem"
  }}>
      <video ref={videoRef} src={src} poster={poster} autoPlay loop muted={muted} playsInline preload="metadata" style={{
    width: "100%",
    display: "block",
    borderRadius
  }} />

      <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 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>;
};

<CameraTermNav
  label="Page sections"
  sections={[
{ label: "Workflows", href: "#choose-the-right-workflow" },
{ label: "Text-to-Video", href: "#text-to-video" },
{ label: "Image-to-Video", href: "#image-to-video" },
{ label: "Keyframes", href: "#keyframes" },
{ label: "Video Continuation", href: "#video-continuation" },
{ label: "References", href: "#workflow-references" },
{ label: "Prompt checklist", href: "#what-to-include-in-a-strong-video-prompt" },
]}
/>

<Tip>
  FLUX 3 supports different Modes based on your Usecase. This Page will give you an Overview about the different Modes and their Capabilites.
</Tip>

## Choose the right workflow

<div className="not-prose">
  <table className="workflow-table">
    <colgroup>
      <col style={{ width: "19%" }} />

      <col style={{ width: "11%" }} />

      <col style={{ width: "22%" }} />

      <col style={{ width: "48%" }} />
    </colgroup>

    <thead>
      <tr>
        <th>Workflow</th>
        <th>Mode</th>
        <th>Jump</th>
        <th>Best for</th>
      </tr>
    </thead>

    <tbody>
      <tr>
        <td className="workflow-table__name">Text-to-Video</td>
        <td><code className="mode-ref-chip">t2v</code></td>
        <td><a className="jump-btn" href="#text-to-video">Jump to section →</a></td>
        <td className="workflow-table__note">Generating a new scene from scratch</td>
      </tr>

      <tr>
        <td className="workflow-table__name">Image-to-Video</td>
        <td><code className="mode-ref-chip">i2v</code></td>
        <td><a className="jump-btn" href="#image-to-video">Jump to section →</a></td>
        <td className="workflow-table__note">Animating a still image</td>
      </tr>

      <tr>
        <td className="workflow-table__name">Keyframes</td>
        <td><code className="mode-ref-chip">i2v</code></td>
        <td><a className="jump-btn" href="#keyframes">Jump to section →</a></td>
        <td className="workflow-table__note">Choreograph a shot through ordered frames</td>
      </tr>

      <tr>
        <td className="workflow-table__name">Video Continuation</td>
        <td><code className="mode-ref-chip">v2v</code></td>
        <td><a className="jump-btn" href="#video-continuation">Jump to section →</a></td>
        <td className="workflow-table__note">Extend existing Clips</td>
      </tr>

      <tr style={{ opacity: 0.55 }}>
        <td className="workflow-table__name">Video Editing</td>
        <td>—</td>
        <td><span style={{ display: "inline-block", padding: "0.15rem 0.6rem", borderRadius: "999px", border: "1px solid currentColor", fontSize: "0.75rem", fontWeight: 600, letterSpacing: "0.03em" }}>Soon</span></td>

        <td className="workflow-table__note" />
      </tr>

      <tr style={{ opacity: 0.55 }}>
        <td className="workflow-table__name">Omni Reference</td>
        <td>—</td>
        <td><span style={{ display: "inline-block", padding: "0.15rem 0.6rem", borderRadius: "999px", border: "1px solid currentColor", fontSize: "0.75rem", fontWeight: 600, letterSpacing: "0.03em" }}>Soon</span></td>

        <td className="workflow-table__note" />
      </tr>
    </tbody>
  </table>
</div>

## **Text-to-Video**

Describe the shot; FLUX 3 turns the prompt into a clip. Examples:

<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." },
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/9ab75b9ff29236ec8c1184cd9df2faa63a0a25c2.mp4", title: "Sofa sky landing", prompt: "A generous cream-colored fabric sofa falls from a bright sky and lands perfectly in a minimalist living room. It descends in majestic slow motion through soft clouds, cushions rippling in the airstream and throw pillows trailing behind it like loyal satellites, then touches down on the wooden floor with a plush, weighty whumph — a dust ring blooming outward as the pillows land one-two-three into their exact corners and a folded blanket settles last over the armrest." },
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/bd5fcd6e77d0a6355fb2aa96b514f494b9b1629c.mp4", title: "Grazing clouds", prompt: "A highland meadow where a small herd of cumulus clouds has descended to graze, drifting a meter above the grass, trailing thin wisps as they crop the turf bald in slow patches. Hold a static telephoto wildlife shot, heat-haze shimmer, absolutely matter-of-fact. Soft overcast light, muted greens. The only sound is wind, distant sheep bells, and a low woolly rumble whenever a cloud tears up grass." },
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/e1b796b83dd6dd999c5baf7e333c3c85e6adf1ed.mp4", title: "Tram of light", prompt: "An old tram crosses the frame through golden dust, and its lit windows project a chain of light squares that slide along the housefronts, climbing steps and doorways as they travel. Phase 1 (0–3s): 85mm into the low sun down a cobbled street thick with backlit dust; rails glow like two golden wires, a tram bell dings once far off, evening swallows overhead. Phase 2 (3–7s): The tram rolls through frame as a dark silhouette rimmed in gold, and its window-light squares appear on the opposite facades — a procession of bright rectangles wandering across stucco, drainpipes, and a startled cat on a windowsill. Phase 3 (7–10s): The camera lets the tram leave and stays with the last light squares as they stretch, bend around a corner, and slip away; the rumble fades, dust keeps burning in the empty street." },
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/47794c002d4c0515f19ef9cf4a346d39b23cb9b2.mp4", title: "Snow leopard", prompt: "In the Himalayan high country, a snow leopard that dissolves into snow mid-leap and reassembles on landing. Phase 1: An 800mm shot across a wind-scoured couloir at first light; a snow leopard flows along a knife-edge ridge, breath steaming, tail heavy and low, stalking a blue sheep on the far crag. Phase 2: She launches across the void — and at the apex her body unravels into a spindrift of powder snow, rosettes scattering into individual snowflakes that hold her running shape as they cross the gap. Phase 3: The flurry lands and collapses inward, fur and muscle condensing from white powder back into cat, front paws hitting rock in full stride, one ember-green eye forming last; narrator whispers: \"In these mountains... even the snow hunts.\"" },
{ video: "https://cdn.sanity.io/files/2gpum2i6/production/7e78359903233dc577dda5ccfcf98779c70ef15e.mp4", title: "Frozen lake, red coat", prompt: "Prompt: …" },
]}
  />
</div>

## **Image-to-Video**

Start from a still image and describe how it should move. The source image sets the first frame; the prompt drives the motion.

<div className="flux3-usecase-slides">
  <AnimateSlider
    items={[
{ image: "https://cdn.sanity.io/images/2gpum2i6/production/ffeaa0bcdf811ccf4d87ad549225ad2dbf042dd6-2944x1648.png", video: "https://cdn.sanity.io/files/2gpum2i6/production/e237f8a9f6cf7259a9f66f6ab471493404cbb532.mp4", alt: "Black horse galloping through the desert at dusk, chased by a sports car", title: "Example 1", prompt: "The black horse bursts into a full gallop, mane and tail whipping in the wind, hooves kicking up huge plumes of dust as the sports car chases close behind with headlights flaring. The camera races alongside at ground level, shaking with speed. Thundering hoofbeats, roaring engine, rushing wind." },
{ image: "https://cdn.sanity.io/images/2gpum2i6/production/58430b2254241f059ce59ec01ed4b11ad9fce95e-1280x704.jpg", video: "https://cdn.sanity.io/files/2gpum2i6/production/ce0f3c021525c568ad591daf681f3d9c601f21aa.mp4", alt: "Image-to-video example 2", title: "Example 2" },
{ image: "https://cdn.sanity.io/images/2gpum2i6/production/2a418d0b787e96624ade85707bff5100e9742838-1280x736.jpg", video: "https://cdn.sanity.io/files/2gpum2i6/production/b993a73e27a11f6ba0e64f25616cb0786685085c.mp4", alt: "Image-to-video example 3", title: "Example 3" },
{ image: "https://cdn.sanity.io/images/2gpum2i6/production/b015fdbe652ebfac7e834067fe9091d670f35cff-1280x736.png", video: "https://cdn.sanity.io/files/2gpum2i6/production/3bf3b94bfd068396e9b7b7c73421c5316fbc3048.mp4", alt: "Image-to-video example 4", title: "Example 4" },
{ image: "https://cdn.sanity.io/images/2gpum2i6/production/a121750cb4422945a6d3ff0eed246271750e3c63-1280x704.jpg", video: "https://cdn.sanity.io/files/2gpum2i6/production/9b7469e7f349b4f26fe9809b9faa13567504aa84.mp4", alt: "Image-to-video example 5", title: "Example 5" },
]}
  />
</div>

## **Keyframes**

Pass several stills as ordered keyframes and FLUX 3 interpolates one continuous shot that moves through each in turn. Each example shows the pinned input frames, then the interpolated result.

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

## **Video Continuation**

Feed an existing clip and continue it. The original sets the motion, subjects, and look; the prompt guides how the shot keeps unfolding without a cut.

<Tabs>
  <Tab title="Elephants">
    <VideoExtension src="https://cdn.sanity.io/files/2gpum2i6/production/08c30d095b03c80ebec84e8a547c3ea694c2efca.mp4" originalDuration={5} alt="Elephants walking toward camera at sunset" />

    ```text wrap theme={null}
    A herd of African elephants walks steadily toward the camera across a dry savanna beneath a huge hazy orange sunset, the animals growing larger in frame as the sun sinks lower behind them.
    ```
  </Tab>

  <Tab title="Penguin">
    <VideoExtension src="https://cdn.sanity.io/files/2gpum2i6/production/c3251e7b67573784027544e16752c517eb73f7b7.mp4" originalDuration={5} alt="Penguin waddling across granite boulders" />

    ```text wrap theme={null}
    An African penguin waddles across sun-warmed granite boulders behind swaying fynbos foliage, then hops down a rock shelf and pushes on between the boulders as the handheld camera follows.
    ```
  </Tab>

  <Tab title="Hiking couple">
    <VideoExtension src="https://cdn.sanity.io/files/2gpum2i6/production/3c732c7e2b1e1d42d062b5399a5c92e196207b6e.mp4" originalDuration={5} alt="Hiking boots crossing an alpine ridge" />

    ```text wrap theme={null}
    A low ground-level view of hiking boots and trekking poles stepping across a rocky alpine ridge continues, a second hiker's boots following through the frame as the misty peak looms beyond.
    ```
  </Tab>

  <Tab title="Iguazú Falls">
    <VideoExtension src="https://cdn.sanity.io/files/2gpum2i6/production/69c7cd8f26d64630880b5e330581cb2312f0645c.mp4" originalDuration={5} alt="Waterfall curtains pounding into a plunge pool" />

    ```text wrap theme={null}
    A thundering close view of massive waterfall curtains pounds on continuously, mist billowing from the plunge pool as a faint rainbow arc glimmers in and out of the drifting spray.
    ```
  </Tab>

  <Tab title="Giraffe">
    <VideoExtension src="https://cdn.sanity.io/files/2gpum2i6/production/d1368e9ad2193e8e85b0165b6fa793e4920546f1.mp4" originalDuration={5} alt="Giraffe ambling across dry scrubland" />

    ```text wrap theme={null}
    A giraffe in dry scrubland, framed through soft out-of-focus branches, surveys the plain and then begins an unhurried ambling walk across the frame behind the swaying foliage.
    ```
  </Tab>

  <Tab title="SUV drift">
    <VideoExtension src="https://cdn.sanity.io/files/2gpum2i6/production/8f587923157cc33e6558fdd4d6c0a96ff6a6fdc8.mp4" originalDuration={5} alt="Dark SUV drifting across a dusty flat" />

    ```text wrap theme={null}
    A dark SUV drifting across a dusty construction flat in front of unfinished high-rises swings through another wide slide toward the camera, dust boiling off its tires as it powers past.
    ```
  </Tab>
</Tabs>

## Workflow references

<CardGroup cols={2}>
  <Card title="Text-to-Video" icon="film" href="/guides/prompting_video_text_to_video">
    Learn how to prompt FLUX 3 for action, camera movement, pacing, and cleaner shot logic.
  </Card>

  <Card title="Audio and speech" icon="waveform-lines" href="/guides/prompting_video_audio">
    Direct dialogue, voiceover, ambience, effects, music, and the shape of a voice.
  </Card>

  <Card title="Examples & Cheatsheet" icon="camera" href="/guides/prompting_video_camera_terms">
    Reference framing, angle, composition, movement, and focus language you can reuse directly in prompts.
  </Card>

  <Card title="FLUX 3 Video" icon="film" href="/flux_3/flux3_video">
    Text-to-video, image-to-video with keyframes, video continuation, and
    synchronized audio.
  </Card>

  <Card title="Prompting Basics" icon="pen" href="/guides/prompting_unified_basics">
    Core prompting principles that still apply before you layer in motion and camera language.
  </Card>

  <Card title="Image Editing Guide" icon="pen-to-square" href="/guides/prompting_editing_overview">
    Use this when your workflow depends more on reference control than on pure generation.
  </Card>
</CardGroup>

## What to include in a strong video prompt

1. **Subject and action**: Who or what is moving, and what exactly happens.
2. **Camera direction**: Static shot, slow push-in, handheld follow, overhead drift, or rapid pan.
3. **Scene and atmosphere**: Environment, lighting, weather, time of day, and mood.
4. **Motion qualities**: Slow, abrupt, weightless, chaotic, precise, cinematic, documentary.
5. **Continuity constraints**: What must remain stable across the clip, especially for edits or extensions.
