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

# FLUX 3 Video

> Generate video with synchronized audio using FLUX 3, edit an existing clip with FLUX Video Edit [fast], or run FLUX 3 Action, the open-weights action mode.

export const VideoReelMosaic = ({title = "Generated with FLUX 3", rows = [], speed = 6.5, itemWidth = "clamp(14rem, 20vw, 18rem)", itemAspectRatio = "16 / 9", margin = "2.75rem 0 3.5rem", showTopBorder = true, paddingTop = "2rem"}) => {
  const preparedRows = rows.filter(row => Array.isArray(row) && row.length > 0);
  const startVideo = event => {
    const video = event.currentTarget;
    video.muted = true;
    const playback = video.play?.();
    if (playback && typeof playback.catch === "function") playback.catch(() => {});
  };
  return <div className="not-prose flux3-video-reel-mosaic" style={{
    margin
  }}>
      <style>{`
        @keyframes flux3VideoReelLeft {
          from { transform: translateX(0); }
          to { transform: translateX(-50%); }
        }

        @keyframes flux3VideoReelRight {
          from { transform: translateX(-50%); }
          to { transform: translateX(0); }
        }

        .flux3-video-reel-mosaic:hover .flux3-video-reel-mosaic__track {
          animation-play-state: paused;
        }

        .flux3-video-reel-mosaic__frame {
          border-top: 1px solid rgba(22,49,38,0.14);
        }
        .dark .flux3-video-reel-mosaic__frame {
          border-top-color: rgba(246,241,231,0.12);
        }
        .flux3-video-reel-mosaic__frame--borderless {
          border-top: none;
        }

        .flux3-video-reel-mosaic__title {
          color: var(--aspen-ink, #163126);
        }
        .dark .flux3-video-reel-mosaic__title {
          color: rgba(246,241,231,0.94);
        }

        .flux3-video-reel-mosaic__fade {
          background: linear-gradient(90deg, var(--aspen-paper, #fffdf9) 0%, transparent 8%, transparent 92%, var(--aspen-paper, #fffdf9) 100%);
        }
        .dark .flux3-video-reel-mosaic__fade {
          background: linear-gradient(90deg, rgba(4,16,11,1) 0%, rgba(4,16,11,0) 8%, rgba(4,16,11,0) 92%, rgba(4,16,11,1) 100%);
        }
      `}</style>

      <div className={`flux3-video-reel-mosaic__frame${showTopBorder ? "" : " flux3-video-reel-mosaic__frame--borderless"}`} style={{
    paddingTop
  }}>
        {title ? <h2 className="flux3-video-reel-mosaic__title" style={{
    margin: "0 0 1.5rem",
    textAlign: "center",
    fontFamily: '"Instrument Sans", sans-serif',
    fontSize: "clamp(2rem, 4vw, 3.15rem)",
    fontWeight: 500,
    letterSpacing: "-0.04em"
  }}>
          {title}
        </h2> : null}

        <div style={{
    position: "relative",
    display: "grid",
    gap: "0.75rem",
    overflow: "hidden"
  }}>
          {preparedRows.map((row, rowIdx) => {
    const direction = rowIdx % 2 === 0 ? "flux3VideoReelLeft" : "flux3VideoReelRight";
    const duration = `${Math.max(row.length * speed, 18)}s`;
    const looped = [...row, ...row];
    return <div key={rowIdx} style={{
      overflow: "hidden"
    }}>
                <div className="flux3-video-reel-mosaic__track" style={{
      display: "flex",
      gap: "0.75rem",
      width: "max-content",
      animation: `${direction} ${duration} linear infinite`
    }}>
                  {looped.map((item, idx) => <div key={`${rowIdx}-${idx}-${item.video}`} style={{
      position: "relative",
      width: item.width || itemWidth,
      aspectRatio: item.height ? undefined : item.aspectRatio || itemAspectRatio,
      height: item.height,
      flexShrink: 0,
      overflow: "hidden",
      borderRadius: "0",
      background: "#08110d"
    }}>
                      {item.image ? <img src={item.image} alt={item.title || title} style={{
      display: "block",
      width: "100%",
      height: "100%",
      objectFit: "cover"
    }} /> : <video src={item.video} poster={item.poster} autoPlay defaultMuted muted loop playsInline preload="auto" onCanPlay={startVideo} onLoadedData={startVideo} aria-label={item.title || title} style={{
      display: "block",
      width: "100%",
      height: "100%",
      objectFit: "contain",
      background: "#08110d"
    }} />}
                    </div>)}
                </div>
              </div>;
  })}

          <span aria-hidden="true" className="flux3-video-reel-mosaic__fade" style={{
    position: "absolute",
    inset: 0,
    pointerEvents: "none"
  }} />
        </div>
      </div>
    </div>;
};

export const FeatureSlider = ({items = []}) => {
  const [active, setActive] = useState({});
  const [inView, setInView] = useState(0);
  const [soundOn, setSoundOn] = useState({});
  const activeOf = cardIdx => active[cardIdx] || 0;
  const goTo = (cardIdx, next) => setActive(prev => ({
    ...prev,
    [cardIdx]: next
  }));
  const step = (cardIdx, dir, n) => setActive(prev => ({
    ...prev,
    [cardIdx]: ((prev[cardIdx] || 0) + dir + n) % n
  }));
  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 = v.dataset.soundOn !== "1";
          const p = v.play?.();
          if (p && typeof p.catch === "function") p.catch(() => {});
        } else {
          v.pause?.();
        }
      });
    }, {
      threshold: 0.4
    });
    return observerRef.current;
  };
  const videoRefs = useRef({});
  const attachVideo = (el, cardIdx) => {
    if (!el) return;
    videoRefs.current[cardIdx] = el;
    const io = getObserver();
    if (io) io.observe(el);
  };
  const toggleSound = cardIdx => {
    const turningOn = !soundOn[cardIdx];
    const el = videoRefs.current[cardIdx];
    if (el) {
      el.dataset.soundOn = turningOn ? "1" : "0";
      el.muted = !turningOn;
      if (turningOn) {
        const p = el.play?.();
        if (p && typeof p.catch === "function") p.catch(() => {});
      }
    }
    setSoundOn(prev => ({
      ...prev,
      [cardIdx]: turningOn
    }));
  };
  const trackRef = useRef(null);
  const cardRefs = useRef([]);
  const measuredRef = useRef(false);
  const [edges, setEdges] = useState({
    left: false,
    right: true
  });
  const [hasOverflow, setHasOverflow] = useState(true);
  const updateEdges = el => {
    if (!el) return;
    const max = el.scrollWidth - el.clientWidth;
    setHasOverflow(prev => prev === max > 8 ? prev : max > 8);
    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 = video.dataset.soundOn !== "1";
    const p = video.play?.();
    if (p && typeof p.catch === "function") p.catch(() => {});
  };
  const renderIO = io => {
    const parts = io.split(/→|->/);
    return parts.map((p, i) => <span key={i} className="feature-slide__io-part">
        <span>{p.trim()}</span>
        {i < parts.length - 1 && <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
            <line x1="5" y1="12" x2="19" y2="12" />
            <polyline points="12 5 19 12 12 19" />
          </svg>}
      </span>);
  };
  const renderMedia = (ex, title, cardIdx, exIdx, soundIsOn) => ex.video ? <video key={`v-${cardIdx}-${exIdx}`} ref={el => attachVideo(el, cardIdx)} src={ex.video} poster={ex.poster || ex.img} loop muted={!soundIsOn} data-sound-on={soundIsOn ? "1" : "0"} playsInline preload="metadata" onLoadedData={startVideo} onCanPlay={startVideo} aria-label={title || ""} className="feature-slide__media-el" /> : <img key={`i-${cardIdx}-${exIdx}`} src={ex.img} alt={title || ""} draggable={false} className="feature-slide__media-el" />;
  return <div className="not-prose feature-slider">
      {hasOverflow && <button type="button" className="feature-slider__arrow feature-slider__arrow--prev" onClick={() => scrollByCard(-1)} disabled={!edges.left} aria-label="Previous feature">
          <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="feature-slider__track" ref={attachTrack} onScroll={e => updateEdges(e.currentTarget)}>
        {items.map((item, idx) => {
    const examples = item.examples && item.examples.length ? item.examples : [{
      video: item.video,
      img: item.img,
      poster: item.poster
    }];
    const n = examples.length;
    const cur = activeOf(idx);
    const ex = examples[Math.min(cur, n - 1)];
    const soundIsOn = !!soundOn[idx];
    const linkLabel = item.linkLabel || (item.eyebrow ? `Explore ${item.eyebrow}` : "Explore");
    return <article key={idx} className="feature-slide" ref={el => {
      cardRefs.current[idx] = el;
    }}>
              <div className="feature-slide__media" onTouchStart={e => {
      __fsMediaTouchStartX = e.touches[0].clientX;
    }} onTouchEnd={e => {
      if (n < 2) return;
      const dx = e.changedTouches[0].clientX - __fsMediaTouchStartX;
      if (dx < -40) step(idx, 1, n); else if (dx > 40) step(idx, -1, n);
    }}>
                {renderMedia(ex, item.title, idx, cur, soundIsOn)}
                <span className="feature-slide__wash" />

                {ex.video && <button type="button" onClick={() => toggleSound(idx)} aria-label={soundIsOn ? "Mute" : "Unmute"} aria-pressed={soundIsOn} className="feature-slide__media-btn feature-slide__sound-btn" style={{
      position: "absolute",
      top: "0.6rem",
      right: "0.6rem",
      zIndex: 2
    }}>
                    {soundIsOn ? <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.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> : <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.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>}
                  </button>}

                {n > 1 && <>
                    <div className="feature-slide__media-arrows">
                      <button type="button" onClick={() => step(idx, -1, n)} aria-label="Previous example" className="feature-slide__media-btn">
                        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                          <polyline points="15 18 9 12 15 6" />
                        </svg>
                      </button>
                      <button type="button" onClick={() => step(idx, 1, n)} aria-label="Next example" className="feature-slide__media-btn">
                        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                          <polyline points="9 18 15 12 9 6" />
                        </svg>
                      </button>
                    </div>
                    <div className="feature-slide__media-dots">
                      {examples.map((_, exIdx) => <button key={exIdx} type="button" onClick={() => goTo(idx, exIdx)} aria-label={`Show example ${exIdx + 1}`} className={`feature-slide__media-dot${exIdx === cur ? " is-active" : ""}`} />)}
                    </div>
                  </>}
              </div>

              <div className="feature-slide__body">
                {item.eyebrow && <span className="feature-slide__eyebrow">{item.eyebrow}</span>}
                {item.title && <h3 className="feature-slide__title">{item.title}</h3>}
                {item.io && <span className="feature-slide__io">{renderIO(item.io)}</span>}
                {item.desc && <p className="feature-slide__desc">{item.desc}</p>}
                {item.href && <a className="feature-slide__link" href={item.href}>
                    {linkLabel}
                    <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                      <line x1="5" y1="12" x2="19" y2="12" />
                      <polyline points="12 5 19 12 12 19" />
                    </svg>
                  </a>}
              </div>
            </article>;
  })}
      </div>

      {hasOverflow && <button type="button" className="feature-slider__arrow feature-slider__arrow--next" onClick={() => scrollByCard(1)} disabled={!edges.right} aria-label="Next feature">
          <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>}

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

export const RequestLifecycle = ({resultVideo = "https://cdn.sanity.io/files/2gpum2i6/production/9b5ab883bfd9c68fec643908cd283357954b8187.mp4", apiKeyHref = "https://dashboard.bfl.ai"}) => {
  const MODES = [{
    uid: "t2v",
    id: "t2v",
    name: "Text to Video",
    json: [],
    js: []
  }, {
    uid: "i2v-still",
    id: "i2v",
    name: "Animate a Still",
    json: ['"keyframes": "https://…/start.jpg",'],
    js: ['keyframes: "https://…/start.jpg",']
  }, {
    uid: "i2v-pair",
    id: "i2v",
    name: "Start + End Frame",
    json: ['"keyframes": ["https://…/a.jpg", "https://…/b.jpg"],'],
    js: ['keyframes: ["https://…/a.jpg", "https://…/b.jpg"],']
  }, {
    uid: "i2v-timed",
    id: "i2v",
    name: "Timestamped Keyframes",
    json: ['"keyframes": [[0, "https://…/a.jpg"], [2.5, "https://…/b.jpg"], [5, "https://…/c.jpg"]],'],
    js: ['keyframes: [[0, "https://…/a.jpg"], [2.5, "https://…/b.jpg"], [5, "https://…/c.jpg"]],']
  }, {
    uid: "v2v",
    id: "v2v",
    name: "Video Continuation",
    json: ['"start_video": "https://…/clip.mp4",'],
    js: ['start_video: "https://…/clip.mp4",']
  }];
  const indentJoin = (lines, pad) => lines.length ? "\n" + pad + lines.join("\n" + pad) : "";
  const submitCode = (m, lang) => {
    const modeList = "t2v, i2v, v2v";
    if (lang === "python") {
      return `import os, requests

resp = requests.post(
    "https://api.bfl.ai/v1/flux-3-video",
    headers={
        "x-key": os.environ["BFL_API_KEY"],
        "Content-Type": "application/json",
    },
    json={
        "mode": "${m.id}",${m.json.length ? "" : "                 # " + modeList}
        "prompt": "a fox running through dawn mist",${indentJoin(m.json, "        ")}
        "resolution": "hd",           # or fhd
        "duration": 5,                # 5–20, or "auto"
        "generate_audio": True,       # synchronized sound, same call
    },
).json()

request_id = resp["id"]
polling_url = resp["polling_url"]   # always poll this exact URL`;
    }
    if (lang === "typescript") {
      return `const resp = await fetch("https://api.bfl.ai/v1/flux-3-video", {
  method: "POST",
  headers: {
    "x-key": process.env.BFL_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    mode: "${m.id}",${m.js.length ? "" : "              // " + modeList}
    prompt: "a fox running through dawn mist",${indentJoin(m.js, "    ")}
    resolution: "hd",            // or fhd
    duration: 5,                 // 5-20, or "auto"
    generate_audio: true,        // synchronized sound, same call
  }),
}).then((r) => r.json());

const { id: requestId, polling_url: pollingUrl } = resp;`;
    }
    return `request=$(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": "${m.id}",
    "prompt": "a fox running through dawn mist",${indentJoin(m.json, "    ")}
    "resolution": "hd",
    "duration": 5,
    "generate_audio": true
  }')

polling_url=$(echo $request | jq -r .polling_url)`;
  };
  const pollCode = lang => {
    if (lang === "python") {
      return `import time

while True:
    time.sleep(1)
    result = requests.get(
        polling_url,
        headers={"x-key": os.environ["BFL_API_KEY"]},
    ).json()

    if result["status"] == "Ready":
        print("Video:", result["result"]["sample"])
        break
    if result["status"] in ("Error", "Failed"):
        print("Generation failed:", result)
        break`;
    }
    if (lang === "typescript") {
      return `while (true) {
  await new Promise((r) => setTimeout(r, 1000));

  const result = await fetch(pollingUrl, {
    headers: { "x-key": process.env.BFL_API_KEY },
  }).then((r) => r.json());

  if (result.status === "Ready") {
    console.log("Video:", result.result.sample);
    break;
  }
  if (["Error", "Failed"].includes(result.status)) {
    console.error("Generation failed:", result);
    break;
  }
}`;
    }
    return `while true; do
  sleep 1
  result=$(curl -s -X GET "$polling_url" -H "x-key: $BFL_API_KEY")
  status=$(echo $result | jq -r .status)
  echo "Status: $status"

  if [ "$status" = "Ready" ]; then
    echo "Video: $(echo $result | jq -r .result.sample)"
    break
  elif [ "$status" = "Error" ] || [ "$status" = "Failed" ]; then
    echo "Generation failed: $result"
    break
  fi
done`;
  };
  const cream = "rgba(246, 241, 231, 0.94)";
  const muted = "rgba(246, 241, 231, 0.46)";
  const faint = "rgba(246, 241, 231, 0.09)";
  const fainter = "rgba(246, 241, 231, 0.05)";
  const mono = '"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace';
  const sans = '"Instrument Sans", sans-serif';
  const accent = "#a6c98a";
  const syn = {
    kw: "#c98aa6",
    str: "#a9c98a",
    num: "#e3b467",
    com: "rgba(246,241,231,0.32)",
    punc: "rgba(246,241,231,0.55)",
    fn: "#7bb0c4"
  };
  const KW = {
    python: "import|from|while|True|False|None|in|if|break|def|return|and|or|not",
    typescript: "const|let|await|new|while|true|false|null|return|if|function|includes",
    curl: "curl|while|do|done|if|then|elif|else|fi|echo|break|sleep"
  };
  const highlightLine = (line, lang) => {
    const marker = lang === "typescript" ? "//" : "#";
    let codePart = line;
    let commentPart = "";
    const ci = line.indexOf(marker);
    if (ci >= 0) {
      const before = line.slice(0, ci);
      if ((before.match(/"/g) || []).length % 2 === 0) {
        codePart = before;
        commentPart = line.slice(ci);
      }
    }
    const kw = KW[lang] || KW.python;
    const re = new RegExp('("(?:[^"\\\\]|\\\\.)*")|(\'(?:[^\'\\\\]|\\\\.)*\')|(\\b\\d[\\w]*\\b)|(\\b(?:' + kw + ')\\b)|([A-Za-z_]\\w*)(?=\\()', "g");
    const tokens = [];
    let last = 0;
    let mm;
    while ((mm = re.exec(codePart)) !== null) {
      if (mm.index > last) tokens.push({
        t: codePart.slice(last, mm.index),
        c: syn.punc
      });
      let color = cream;
      if (mm[1] || mm[2]) color = syn.str; else if (mm[3]) color = syn.num; else if (mm[4]) color = syn.kw; else if (mm[5]) color = syn.fn;
      tokens.push({
        t: mm[0],
        c: color
      });
      last = mm.index + mm[0].length;
    }
    if (last < codePart.length) tokens.push({
      t: codePart.slice(last),
      c: syn.punc
    });
    if (commentPart) tokens.push({
      t: commentPart,
      c: syn.com
    });
    return tokens;
  };
  const renderCode = (code, lang) => {
    const lines = code.split("\n");
    return lines.map((ln, i) => <div key={i} style={{
      minHeight: "1.05rem"
    }}>
        {ln === "" ? "​" : highlightLine(ln, lang).map((tk, j) => <span key={j} style={{
      color: tk.c
    }}>
                {tk.t}
              </span>)}
      </div>);
  };
  const [modeId, setModeId] = useState("t2v");
  const [stage, setStage] = useState("submit");
  const [lang, setLang] = useState("python");
  const [copied, setCopied] = useState(false);
  const mode = MODES.find(m => m.uid === modeId) || MODES[0];
  const stageIndex = stage === "submit" ? 0 : stage === "poll" ? 1 : 2;
  const code = stage === "submit" ? submitCode(mode, lang) : stage === "poll" ? pollCode(lang) : "";
  const copy = () => {
    try {
      navigator.clipboard.writeText(code);
      setCopied(true);
      setTimeout(() => setCopied(false), 1600);
    } catch (e) {}
  };
  const STEPS = [{
    key: "submit",
    label: "Submit",
    color: "#e3b467"
  }, {
    key: "poll",
    label: "Poll",
    color: "#7bb0c4"
  }, {
    key: "ready",
    label: "Ready",
    color: "#6cc38a"
  }];
  const langs = [{
    id: "python",
    label: "Python"
  }, {
    id: "typescript",
    label: "TypeScript"
  }, {
    id: "curl",
    label: "cURL"
  }];
  const Segmented = ({options, value, onChange, size = "md"}) => <div style={{
    display: "inline-flex",
    padding: "0.2rem",
    gap: "0.15rem",
    borderRadius: "999px",
    border: `1px solid ${faint}`,
    background: fainter
  }}>
      {options.map(o => {
    const active = o.id === value;
    return <button key={o.id} type="button" onClick={() => onChange(o.id)} title={o.title} style={{
      padding: size === "sm" ? "0.24rem 0.6rem" : "0.3rem 0.7rem",
      borderRadius: "999px",
      border: "none",
      cursor: "pointer",
      fontFamily: o.mono ? mono : sans,
      fontSize: size === "sm" ? "0.76rem" : "0.8rem",
      fontWeight: active ? 600 : 500,
      letterSpacing: o.mono ? "0" : "0.01em",
      color: active ? "#0c1712" : muted,
      background: active ? accent : "transparent",
      transition: "color 140ms ease, background 140ms ease"
    }}>
            {o.label}
          </button>;
  })}
    </div>;
  return <div className="not-prose req-flow" style={{
    borderRadius: "1rem",
    border: `1px solid ${faint}`,
    background: "linear-gradient(180deg, #0e1a14 0%, #0a120e 100%)",
    color: cream,
    overflow: "hidden",
    boxShadow: "0 24px 60px -30px rgba(0,0,0,0.7)"
  }}>
      {}
      <div style={{
    display: "flex",
    alignItems: "center",
    justifyContent: "space-between",
    gap: "1rem",
    flexWrap: "wrap",
    padding: "0.95rem 1.15rem"
  }}>
        <div style={{
    display: "flex",
    alignItems: "center",
    gap: "0.35rem"
  }}>
          {STEPS.map((s, i) => {
    const done = i < stageIndex;
    const current = i === stageIndex;
    const col = done ? "#6cc38a" : current ? s.color : muted;
    return <div key={s.key} style={{
      display: "flex",
      alignItems: "center",
      gap: "0.35rem"
    }}>
                <button type="button" onClick={() => setStage(s.key)} style={{
      display: "inline-flex",
      alignItems: "center",
      gap: "0.5rem",
      padding: "0.28rem 0.6rem 0.28rem 0.3rem",
      borderRadius: "999px",
      background: current ? "rgba(255,255,255,0.05)" : "transparent",
      border: "none",
      cursor: "pointer",
      transition: "background 140ms ease"
    }}>
                  <span style={{
      display: "inline-flex",
      alignItems: "center",
      justifyContent: "center",
      width: "1.35rem",
      height: "1.35rem",
      borderRadius: "999px",
      border: `1.5px solid ${col}`,
      color: done ? "#0c1712" : col,
      background: done ? "#6cc38a" : "transparent",
      fontFamily: mono,
      fontSize: "0.68rem",
      fontWeight: 700
    }}>
                    {done ? "✓" : i + 1}
                  </span>
                  <span style={{
      fontFamily: sans,
      fontSize: "0.86rem",
      fontWeight: 600,
      color: current || done ? cream : muted
    }}>
                    {s.label}
                  </span>
                </button>
                {i < STEPS.length - 1 ? <span style={{
      width: "1.6rem",
      height: "1px",
      background: faint
    }} /> : null}
              </div>;
  })}
        </div>

        <a href={apiKeyHref} target="_blank" rel="noreferrer" style={{
    display: "inline-flex",
    alignItems: "center",
    gap: "0.4rem",
    padding: "0.4rem 0.85rem",
    borderRadius: "999px",
    border: "none",
    background: accent,
    color: "#0c1712",
    fontFamily: sans,
    fontSize: "0.82rem",
    fontWeight: 600,
    textDecoration: "none",
    whiteSpace: "nowrap"
  }}>
          Get API key
          <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
            <path d="M7 17 17 7" />
            <path d="M7 7h10v10" />
          </svg>
        </a>
      </div>

      {}
      <div style={{
    height: "2px",
    background: faint
  }}>
        <div style={{
    height: "100%",
    width: `${(stageIndex + 1) / 3 * 100}%`,
    background: `linear-gradient(90deg, ${STEPS[0].color}, ${STEPS[stageIndex].color})`,
    transition: "width 300ms cubic-bezier(0.4,0,0.2,1)"
  }} />
      </div>

      <div style={{
    padding: "1.15rem 1.15rem 1.25rem"
  }}>
        {}
        {stage === "submit" ? <div style={{
    display: "flex",
    alignItems: "center",
    gap: "0.7rem",
    flexWrap: "wrap",
    marginBottom: "1rem"
  }}>
            <span style={{
    fontFamily: sans,
    fontSize: "0.72rem",
    fontWeight: 700,
    letterSpacing: "0.09em",
    textTransform: "uppercase",
    color: muted
  }}>
              Mode
            </span>
            <Segmented size="sm" value={modeId} onChange={setModeId} options={MODES.map(m => ({
    id: m.uid,
    label: m.name,
    title: m.name + " (" + m.id + ")",
    mono: false
  }))} />
            <span style={{
    fontFamily: sans,
    fontSize: "0.84rem",
    color: cream,
    fontWeight: 500
  }}>
              {mode.name}
            </span>
          </div> : null}

        {stage === "poll" ? <p style={{
    margin: "0 0 1rem",
    fontFamily: sans,
    fontSize: "0.86rem",
    lineHeight: 1.5,
    color: muted
  }}>
            One loop for every mode — poll the{" "}
            <code style={{
    fontFamily: mono,
    color: cream
  }}>polling_url</code> once a second until the status is{" "}
            <code style={{
    fontFamily: mono,
    color: "#6cc38a"
  }}>Ready</code>.
          </p> : null}

        {}
        {stage !== "ready" ? <div style={{
    borderRadius: "0.7rem",
    border: `1px solid ${faint}`,
    overflow: "hidden",
    background: "#0a0f0c"
  }}>
            {}
            <div style={{
    display: "flex",
    alignItems: "center",
    justifyContent: "space-between",
    gap: "0.75rem",
    flexWrap: "wrap",
    padding: "0.6rem 0.75rem",
    borderBottom: `1px solid ${faint}`,
    background: "rgba(255,255,255,0.015)"
  }}>
              <div style={{
    display: "flex",
    gap: "0.4rem",
    alignItems: "center"
  }}>
                <span style={{
    width: "0.7rem",
    height: "0.7rem",
    borderRadius: "999px",
    background: "#ff5f57"
  }} />
                <span style={{
    width: "0.7rem",
    height: "0.7rem",
    borderRadius: "999px",
    background: "#febc2e"
  }} />
                <span style={{
    width: "0.7rem",
    height: "0.7rem",
    borderRadius: "999px",
    background: "#28c840"
  }} />
              </div>
              <div style={{
    display: "flex",
    alignItems: "center",
    gap: "0.6rem"
  }}>
                <Segmented size="sm" value={lang} onChange={setLang} options={langs} />
                <button type="button" onClick={copy} aria-label="Copy code" style={{
    display: "inline-flex",
    alignItems: "center",
    gap: "0.35rem",
    padding: "0.32rem 0.65rem",
    borderRadius: "0.5rem",
    border: `1px solid ${faint}`,
    background: "transparent",
    color: copied ? "#6cc38a" : muted,
    fontFamily: sans,
    fontSize: "0.76rem",
    fontWeight: 600,
    cursor: "pointer",
    transition: "color 140ms ease"
  }}>
                  {copied ? <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                      <polyline points="20 6 9 17 4 12" />
                    </svg> : <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                      <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
                      <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
                    </svg>}
                  {copied ? "Copied" : "Copy"}
                </button>
              </div>
            </div>
            <pre style={{
    margin: 0,
    padding: "1rem 1.1rem",
    maxHeight: "22rem",
    overflow: "auto",
    background: "transparent",
    fontFamily: mono,
    fontSize: "0.82rem",
    lineHeight: 1.6
  }}>
              <code style={{
    fontFamily: mono,
    whiteSpace: "pre"
  }}>{renderCode(code, lang)}</code>
            </pre>
          </div> : <div style={{
    display: "grid",
    gridTemplateColumns: "minmax(0, 1fr) 9.5rem",
    gap: "1.25rem",
    alignItems: "center"
  }}>
            <div style={{
    display: "flex",
    flexDirection: "column",
    gap: "0.7rem",
    minWidth: 0
  }}>
              <span style={{
    display: "inline-flex",
    alignItems: "center",
    gap: "0.45rem",
    alignSelf: "flex-start",
    padding: "0.3rem 0.75rem",
    borderRadius: "999px",
    border: "1px solid rgba(108,195,138,0.5)",
    background: "rgba(108,195,138,0.14)",
    color: "#6cc38a",
    fontFamily: sans,
    fontSize: "0.82rem",
    fontWeight: 600
  }}>
                <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                  <polyline points="20 6 9 17 4 12" />
                </svg>
                status: Ready
              </span>
              <div style={{
    fontFamily: mono,
    fontSize: "0.82rem",
    lineHeight: 1.7,
    color: muted
  }}>
                <div>
                  <span style={{
    color: "#7bb0c4"
  }}>GET</span>{" "}
                  <span style={{
    color: cream
  }}>polling_url</span> →{" "}
                  <span style={{
    color: "#6cc38a"
  }}>Ready</span>
                </div>
                <div>
                  <span style={{
    color: syn.punc
  }}>result.sample</span> →{" "}
                  <span style={{
    color: "#a9c98a",
    wordBreak: "break-all"
  }}>https://delivery…/out.mp4</span>
                </div>
              </div>
              <p style={{
    margin: "0.1rem 0 0",
    fontFamily: sans,
    fontSize: "0.84rem",
    lineHeight: 1.5,
    color: muted
  }}>
                The signed result URL is valid for about 10 minutes — download it promptly.
              </p>
            </div>
            <div style={{
    position: "relative",
    aspectRatio: "3 / 4",
    borderRadius: "0.6rem",
    overflow: "hidden",
    background: "#08110d",
    border: `1px solid ${faint}`,
    boxShadow: "0 16px 40px -24px rgba(0,0,0,0.9)"
  }}>
              <video src={resultVideo} autoPlay loop muted playsInline preload="metadata" aria-hidden="true" style={{
    position: "absolute",
    inset: 0,
    width: "100%",
    height: "100%",
    objectFit: "cover",
    display: "block"
  }} />
            </div>
          </div>}
      </div>
    </div>;
};

export const DraftTimeline = ({draftSeconds, enhanceSeconds, fullSeconds, title = "Generation time", note = "Bar lengths are measured submit-to-Ready times for the same prompt and settings at hd, shown relative to the draft preview. Absolute times vary with load."}) => {
  const ratio = s => `${(s / draftSeconds).toFixed(1).replace(/\.0$/, "")}x`;
  const max = Math.max(fullSeconds, draftSeconds + (enhanceSeconds || 0)) * 1.08;
  const rows = [{
    label: "Draft preview",
    sub: "draft: true",
    segments: [{
      from: 0,
      len: draftSeconds,
      kind: "draft",
      tag: "baseline"
    }]
  }, {
    label: "Full render",
    sub: "one request",
    segments: [{
      from: 0,
      len: fullSeconds,
      kind: "full",
      tag: ratio(fullSeconds)
    }]
  }];
  if (enhanceSeconds) {
    rows.push({
      label: "Draft, then enhance the winner",
      sub: "draft_enhance",
      segments: [{
        from: 0,
        len: draftSeconds,
        kind: "draft",
        tag: null
      }, {
        from: draftSeconds,
        len: enhanceSeconds,
        kind: "enhance",
        tag: `${ratio(draftSeconds + enhanceSeconds)} total`
      }]
    });
  }
  return <div className="not-prose draft-timeline">
      <style>{`
        .draft-timeline {
          margin: 1.75rem 0;
          border: 1px solid rgba(72,106,88,0.2);
          border-radius: 1rem;
          padding: 1.4rem 1.5rem 1.1rem;
          background: var(--aspen-paper, #fffdf9);
        }
        .dark .draft-timeline {
          background: rgba(72,106,88,0.06);
          border-color: rgba(246,241,231,0.14);
        }
        .draft-timeline__title {
          margin: 0 0 1.1rem;
          font-size: 0.78rem; font-weight: 700;
          letter-spacing: 0.08em; text-transform: uppercase;
          color: var(--aspen-ink, #163126);
        }
        .dark .draft-timeline__title { color: rgba(246,241,231,0.92); }
        .draft-timeline__row { margin-bottom: 1.05rem; }
        .draft-timeline__row:last-of-type { margin-bottom: 0.4rem; }
        .draft-timeline__label {
          display: flex; align-items: baseline; gap: 0.5rem;
          margin-bottom: 0.35rem;
          font-size: 0.86rem; font-weight: 600;
          color: var(--aspen-ink, #163126);
        }
        .dark .draft-timeline__label { color: rgba(246,241,231,0.92); }
        .draft-timeline__sub {
          font-family: "IBM Plex Mono", ui-monospace, monospace;
          font-size: 0.72rem; font-weight: 500;
          color: rgba(22,49,38,0.55);
        }
        .dark .draft-timeline__sub { color: rgba(246,241,231,0.55); }
        .draft-timeline__track {
          position: relative; height: 1.5rem;
          border-radius: 0.4rem;
          background: rgba(72,106,88,0.08);
          overflow: hidden;
        }
        .dark .draft-timeline__track { background: rgba(246,241,231,0.07); }
        .draft-timeline__seg {
          position: absolute; top: 0; bottom: 0;
          display: flex; align-items: center; justify-content: flex-end;
          padding-right: 0.55rem;
          font-family: "IBM Plex Mono", ui-monospace, monospace;
          font-size: 0.72rem; font-weight: 600; white-space: nowrap;
        }
        .draft-timeline__seg--draft { background: var(--aspen-evergreen, #486a58); color: #fffdf9; border-radius: 0.4rem; }
        .draft-timeline__seg--enhance { background: rgba(72,106,88,0.45); color: #fffdf9; border-radius: 0 0.4rem 0.4rem 0; }
        .draft-timeline__seg--full { background: var(--aspen-stone, #b5ae9a); color: var(--aspen-ink, #163126); border-radius: 0.4rem; }
        .draft-timeline__note {
          margin-top: 0.9rem;
          font-size: 0.74rem; line-height: 1.5;
          color: rgba(22,49,38,0.55);
        }
        .dark .draft-timeline__note { color: rgba(246,241,231,0.5); }
      `}</style>

      <p className="draft-timeline__title">{title}</p>

      {rows.map((row, i) => <div key={i} className="draft-timeline__row">
          <div className="draft-timeline__label">
            <span>{row.label}</span>
            <span className="draft-timeline__sub">{row.sub}</span>
          </div>
          <div className="draft-timeline__track">
            {row.segments.map((seg, j) => <span key={j} className={`draft-timeline__seg draft-timeline__seg--${seg.kind}`} style={{
    left: `${seg.from / max * 100}%`,
    width: `${seg.len / max * 100}%`
  }}>
                {seg.tag}
              </span>)}
          </div>
        </div>)}

      <p className="draft-timeline__note">{note}</p>
    </div>;
};

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

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

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

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

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

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

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

<div className="flux3-overview-page flux3-page">
  <VideoReelMosaic
    title="Generated with FLUX 3"
    itemWidth="clamp(14rem, 20vw, 18rem)"
    margin="0 0 2.5rem"
    showTopBorder={false}
    paddingTop="0.5rem"
    rows={[
[
  { video: "https://cdn.sanity.io/files/2gpum2i6/production/b5e1de89047f707c5717ee5d712e19fddbe5884d.mp4" },
  { video: "https://cdn.sanity.io/files/2gpum2i6/production/9fc7558baa172434b09a8935b611ca8f3886266c.mp4" },
  { video: "https://cdn.sanity.io/files/2gpum2i6/production/42c41436937dc187bca741107e014dd56e0caa10.mp4" },
  { video: "https://cdn.sanity.io/files/2gpum2i6/production/b5bf636b03765786e3b6d06d506f1584de93fc24.mp4" },
],
[
  { video: "https://cdn.sanity.io/files/2gpum2i6/production/9b5ab883bfd9c68fec643908cd283357954b8187.mp4" },
  { video: "https://cdn.sanity.io/files/2gpum2i6/production/c600a7611a8d7331a923706f73993a34232cbe14.mp4" },
  { video: "https://cdn.sanity.io/files/2gpum2i6/production/64a6ac67b18f54aea972653b8dec5fbcdbe52b43.mp4" },
  { video: "https://cdn.sanity.io/files/2gpum2i6/production/0179ca5004d5cbbf466f42e2c9a7e3a0b3549317.mp4" },
],
]}
  />

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

    <p>
      One model trained across image, video, and audio.
    </p>
  </div>

  ## One model, multiple modalities

  FLUX 3 generates **video with synchronized audio** from a text prompt, pinned
  keyframes, or an existing clip to continue. FLUX Video Edit \[fast] applies an edit
  instruction to a source clip through a separate FLUX Tools endpoint.

  * **Up to 20 seconds, up to 4K** (3840 × 2176 for 16:9), 24 fps, in a single request.
  * **Multilingual speech with strong lipsync**, plus effects and ambience, generated with the frames.
  * **Multiple scenes and camera angles in one generation.** Shots hold together across cuts.
  * **Stylistic range beyond cinematic:** animation, motion design, stylized artistic looks.
  * **Accurate text and typography** rendered inside the scene.

  <Note>
    FLUX 3 is a **preview** model. **Omni Reference with images and videos** will
    be available soon.
  </Note>

  ## Modes

  Every `/v1/flux-3-video` request names a mode. The mode is how you tell FLUX 3 what you're
  starting from, and it decides what the model does with your media: start a
  clip from words alone, build one around images you pin, carry an existing clip
  forward, or enhance a draft.

  The video modes are:

  * **Text to Video** (`t2v`) starts from nothing but your prompt.
  * **Image to Video** (`i2v`) starts from your images. They become frames of the clip itself.
  * **Video Continuation** (`v2v`) starts from a clip you already have and keeps it going.

  FLUX Video Edit \[fast] is a separate tool at `/v1/flux-tools/video-edit-v1`,
  documented on its [own page](/flux_tools/flux_video_edit). It takes `video` and
  `prompt`, with no `mode` field.

  The public [flux-3-video endpoint](/api-reference/utility/generate-a-video-with-flux-3)
  documents `t2v`, `i2v`, and `v2v`, with different media fields and limits. The first
  three tabs are **one scene passed through the API**: the first request
  generates a clip, and the two after it run on that clip's own frames. Each of
  those tabs shows the request we sent and the video that came back.

  <Tabs>
    <Tab title="Text to Video">
      Describe the shot. `t2v` turns the prompt into a clip, sound included.
      This is the request that started the scene:

      ```bash theme={null}
      curl -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": "she takes his hand and pulls him laughing through the lantern-lit alley, the camera chasing them, paper lanterns swaying overhead, their footsteps and laughter echoing off the walls",
          "duration": 8
        }'
      ```

      <video controls muted playsInline preload="metadata" style={{ width: "100%", display: "block", borderRadius: "0.5rem", marginTop: "1rem" }} src="https://mintcdn.com/bfl/gUyNpVEEjK1_M2Gr/images/flux3/modes-demo/alley.mp4?fit=max&auto=format&n=gUyNpVEEjK1_M2Gr&q=85&s=b64172fd2beca22bc00c5df0caea553d" data-path="images/flux3/modes-demo/alley.mp4" />

      <p style={{ textAlign: "center", marginTop: "0.5rem", opacity: 0.7 }}>The clip this request returned. The next two tabs build on it.</p>
    </Tab>

    <Tab title="Image to Video">
      There is no separate start-frame field: `keyframes` is how you hand `i2v`
      images, and one image is the start frame. We extracted the **opening frame
      of the clip in the first tab** and pinned it, with a new camera direction:

      ```bash theme={null}
      curl -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": "from this frame the camera rises slowly above the alley as they rush away beneath the lanterns, their laughter fading into the night",
          "keyframes": "data:image/png;base64,<opening frame>"
        }'
      ```

      <video controls muted playsInline preload="metadata" style={{ width: "100%", display: "block", borderRadius: "0.5rem", marginTop: "1rem" }} src="https://mintcdn.com/bfl/gUyNpVEEjK1_M2Gr/images/flux3/modes-demo/alley_alt.mp4?fit=max&auto=format&n=gUyNpVEEjK1_M2Gr&q=85&s=b47f41eb8ac928a614c18aa57cf348ac" data-path="images/flux3/modes-demo/alley_alt.mp4" />

      <p style={{ textAlign: "center", marginTop: "0.5rem", opacity: 0.7 }}>Same first frame, different film: the pinned image opens the clip exactly, then the prompt takes over.</p>

      More images, more control: two pin the start and end frames, up to ten
      storyboard the clip, and `[seconds, image]` pairs pin each one to an
      exact moment. Here the hero clip's **first and last frames** are pinned at
      `0` and `8` seconds and the model finds its own path between them:

      ```bash theme={null}
      curl -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": "they sprint the length of the lantern-lit alley, lanterns blurring past, footsteps quick on the wet stones",
          "keyframes": [[0, "data:image/png;base64,<first frame>"], [8, "data:image/png;base64,<last frame>"]],
          "duration": 8
        }'
      ```

      <video controls muted playsInline preload="metadata" style={{ width: "100%", display: "block", borderRadius: "0.5rem", marginTop: "1rem" }} src="https://mintcdn.com/bfl/gUyNpVEEjK1_M2Gr/images/flux3/modes-demo/alley_between.mp4?fit=max&auto=format&n=gUyNpVEEjK1_M2Gr&q=85&s=89b5095f3a8b17d4f62142b41e2ed27d" data-path="images/flux3/modes-demo/alley_between.mp4" />

      <p style={{ textAlign: "center", marginTop: "0.5rem", opacity: 0.7 }}>Both pins hit exactly; the sprint in between is the model's.</p>
    </Tab>

    <Tab title="Video Continuation">
      `v2v` picks up where your clip ends. Momentum, framing, and scene logic
      carry into the new footage. We sent the **first tab's clip** as
      `start_video` and asked for the scene's next beat:

      ```bash theme={null}
      curl -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": "they burst out of the alley into a crowded night market, drums and street chatter swelling, she pulls him into the lantern light",
          "start_video": "data:video/mp4;base64,<the clip from the first tab>",
          "duration": 8
        }'
      ```

      <video controls muted playsInline preload="metadata" style={{ width: "100%", display: "block", borderRadius: "0.5rem", marginTop: "1rem" }} src="https://mintcdn.com/bfl/gUyNpVEEjK1_M2Gr/images/flux3/modes-demo/alley_continued.mp4?fit=max&auto=format&n=gUyNpVEEjK1_M2Gr&q=85&s=39fb34873ddf20e3f746a06d313f8481" data-path="images/flux3/modes-demo/alley_continued.mp4" />

      <p style={{ textAlign: "center", marginTop: "0.5rem", opacity: 0.7 }}>The same couple, out of the alley and into the market: continuation keeps the scene's logic and sound.</p>
    </Tab>

    <Tab title="FLUX Video Edit">
      One prompt asked for two changes to this clip: the pedestrians go and pigeons
      arrive around the fountain. The buildings, the water and the camera move are
      as they were in the source.

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

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

      Send the clip as `video` and the instruction as `prompt` to
      `POST /v1/flux-tools/video-edit-v1` with an `x-key` header:

      ```json theme={null}
      {
        "video": "https://cdn.sanity.io/files/2gpum2i6/production/a38495a2841be9a8d2827d48df5f17aec7b5ac98.mp4",
        "prompt": "Remove all the pedestrians and add a flock of pigeons around the fountain."
      }
      ```

      Poll the returned `polling_url` until `Ready`, then download `result.sample`.
      Every other edit type, with clips, is on the
      [FLUX Video Edit page](/flux_tools/flux_video_edit).
    </Tab>
  </Tabs>

  For `t2v`, `i2v`, and `v2v`, the following optional fields have defaults. Check
  the API reference for each mode's accepted values:

  * **`aspect_ratio`** and **`duration`** default to `auto` and fit themselves to the content.
  * **`resolution`** defaults to `hd`; `fhd`, `qhd`, and `uhd` finish the result at a higher resolution via the video upsampler.
  * **`generate_audio`** defaults to `true`.

  The full field list, constraints, and dimensions live in the
  [API reference](/api-reference/utility/generate-a-video-with-flux-3).

  ## Draft mode

  Iterate in draft, commit once. Drafts generate faster and cost about a third
  of a full render, so you can explore variants freely and only pay full price
  for the shot you keep.

  * **`draft: true`** returns a fast preview instead of a full render, plus a `draft_cache` bundle in the result.
  * **`mode: "draft_enhance"`** renders the preview you picked at full quality: send its bundle as `draft_cache` and the original generation is reproduced. Same shot, same seed, nothing re-interpreted.

  Here is the same prompt run through all three paths:

  <DraftTimeline draftSeconds={61} enhanceSeconds={83} fullSeconds={108} />

  Each fresh submit is its own generation, so the direct full render can
  interpret the shot differently from the draft you liked. Enhancing keeps it:

  <Columns cols={3}>
    <div>
      <video controls muted playsInline preload="metadata" style={{ width: "100%", display: "block", borderRadius: "0.5rem" }} src="https://mintcdn.com/bfl/gUyNpVEEjK1_M2Gr/images/flux3/draft-demo/mocap_draft.mp4?fit=max&auto=format&n=gUyNpVEEjK1_M2Gr&q=85&s=ed697a1f09d420f180c1ffaa804051ea" data-path="images/flux3/draft-demo/mocap_draft.mp4" />

      <p style={{ textAlign: "center", marginTop: "0.5rem", opacity: 0.7 }}>Draft preview</p>
    </div>

    <div>
      <video controls muted playsInline preload="metadata" style={{ width: "100%", display: "block", borderRadius: "0.5rem" }} src="https://mintcdn.com/bfl/gUyNpVEEjK1_M2Gr/images/flux3/draft-demo/mocap_enhanced.mp4?fit=max&auto=format&n=gUyNpVEEjK1_M2Gr&q=85&s=291613d06877c8cadfe3286656f473d1" data-path="images/flux3/draft-demo/mocap_enhanced.mp4" />

      <p style={{ textAlign: "center", marginTop: "0.5rem", opacity: 0.7 }}>Enhanced: the draft's shot, full quality</p>
    </div>

    <div>
      <video controls muted playsInline preload="metadata" style={{ width: "100%", display: "block", borderRadius: "0.5rem" }} src="https://mintcdn.com/bfl/gUyNpVEEjK1_M2Gr/images/flux3/draft-demo/mocap_full.mp4?fit=max&auto=format&n=gUyNpVEEjK1_M2Gr&q=85&s=998207a79cfaa9fb13a99851a421f99a" data-path="images/flux3/draft-demo/mocap_full.mp4" />

      <p style={{ textAlign: "center", marginTop: "0.5rem", opacity: 0.7 }}>Direct full render: its own take</p>
    </div>
  </Columns>

  <Tabs>
    <Tab title="1: Draft">
      ```bash theme={null}
      curl -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",
          "draft": true
        }'
      ```
    </Tab>

    <Tab title="2: Enhance the one you picked">
      Download the winning preview's `draft_cache` URL from its result, then send
      the bundle back:

      ```bash theme={null}
      curl -X POST https://api.bfl.ai/v1/flux-3-video \
        -H "x-key: $BFL_API_KEY" \
        -H "Content-Type: application/json" \
        -d "{
          \"mode\": \"draft_enhance\",
          \"draft_cache\": \"$(base64 -i draft_cache.bin)\"
        }"
      ```
    </Tab>
  </Tabs>

  Limits and constraints are in the [API reference](/api-reference/utility/generate-a-video-with-flux-3).

  ## Specifications

  | Mode | You send | Length | Full render, per second (`hd` / `fhd` / `qhd` / `uhd`) | Draft |
  | - | - | - | - | - |
  | **Text to Video** | a prompt | 5–20 s | \$0.17 / \$0.29 / \$0.40 / \$0.80 | \$0.06/s |
  | **Image to Video** | a prompt + 1–10 images | 5–20 s | \$0.17 / \$0.29 / \$0.40 / \$0.80 | \$0.06/s |
  | **Video Continuation** | a prompt + your clip | 5–15 s | \$0.41 / \$0.53 / \$0.65 / \$0.95 | \$0.12/s |

  Every mode outputs 24 fps at `hd`, `fhd`, `qhd`, or `uhd`, with up to 3840 × 2176
  pixels for 16:9. Supported aspect ratios are 21:9, 2:1, 16:9, 4:3, 1:1, 3:4, 9:16, and 9:21. Drafts
  render at `hd`. Delivered dimensions for every class are in the
  [video documentation](/flux_3/flux3_video#resolution).

  ## What it can do

  <FeatureSlider
    items={[
{
  eyebrow: "Video",
  title: "Motion, from a single line.",
  io: "Text, Image, Video → Video",
  desc: "Generate up to 20 seconds at up to 4K from a prompt, animate a still, choreograph a shot through pinned keyframes, or continue an existing clip. All through the same model.",
  href: "/flux_3/flux3_video",
  examples: [
    { video: "https://cdn.sanity.io/files/2gpum2i6/production/678a974c9a9f41c9619309d06a6bfa508edf05f0.mp4" },
    { video: "https://cdn.sanity.io/files/2gpum2i6/production/bac965ff8ea5d670a4632c56a45abcf9909000c8.mp4" },
    { video: "https://cdn.sanity.io/files/2gpum2i6/production/b07adc41e6eacf5511d832575474fc54dc15000b.mp4" },
    { video: "https://cdn.sanity.io/files/2gpum2i6/production/da815568ca416cd62a392dd1d7fed0e51980a830.mp4" },
    { video: "https://cdn.sanity.io/files/2gpum2i6/production/9b5ab883bfd9c68fec643908cd283357954b8187.mp4" },
    { video: "https://cdn.sanity.io/files/2gpum2i6/production/b5e1de89047f707c5717ee5d712e19fddbe5884d.mp4" },
    { video: "https://cdn.sanity.io/files/2gpum2i6/production/c600a7611a8d7331a923706f73993a34232cbe14.mp4" },
  ],
},
{
  eyebrow: "Image to Video",
  title: "From still to motion.",
  io: "Image → Video",
  desc: "Pin a still as the opening frame, interpolate between a start and end frame, or choreograph a shot through pinned keyframes.",
  href: "/flux_3/flux3_video",
  examples: [
    { video: "https://cdn.sanity.io/files/2gpum2i6/production/3bf3b94bfd068396e9b7b7c73421c5316fbc3048.mp4" },
    { video: "https://cdn.sanity.io/files/2gpum2i6/production/37ae0ee4b6ad3190ea55cc2378bff99a40dad6c1.mp4" },
    { video: "https://cdn.sanity.io/files/2gpum2i6/production/9fc7558baa172434b09a8935b611ca8f3886266c.mp4" },
    { video: "https://cdn.sanity.io/files/2gpum2i6/production/42c41436937dc187bca741107e014dd56e0caa10.mp4" },
    { video: "https://cdn.sanity.io/files/2gpum2i6/production/0179ca5004d5cbbf466f42e2c9a7e3a0b3549317.mp4" },
  ],
},
{
  eyebrow: "Audio",
  title: "Audio and speech.",
  io: "Video with sound, same generation",
  desc: "Audio is on by default: multilingual speech with lipsync, effects, and ambience rendered scene-aware alongside the frames. No second model, no second pass.",
  href: "/flux_3/flux3_video#audio",
  examples: [
    { video: "https://cdn.sanity.io/files/2gpum2i6/production/64b71d1f3a26538b75b70effee5c05b38370448f.mp4" },
    { video: "https://cdn.sanity.io/files/2gpum2i6/production/4a9e0ac0ccf00cb54dfd32606d3dc41a14760e48.mp4" },
    { video: "https://cdn.sanity.io/files/2gpum2i6/production/ba9e8cac2a35ef23a9e5e6d6e2cf930fc467b2cb.mp4" },
    { video: "https://cdn.sanity.io/files/2gpum2i6/production/d3f4192d655774daec57fdae9b626025a6bc914b.mp4" },
    { video: "https://cdn.sanity.io/files/2gpum2i6/production/7dc7fd79a1d7e37157247b66890d479039ad3e05.mp4" },
  ],
},
]}
  />

  ## FLUX 3 Image

  FLUX 3 Image generates and edits images through one endpoint,
  `/v1/flux-3-image`. Send a prompt to generate, or add up to ten reference
  images to edit, restyle, or combine them. Bounding boxes in the prompt place
  each element. Output goes up to 4K. See the
  [FLUX 3 Image overview](/flux_3/flux3_image_overview).

  ## FLUX 3 Action

  FLUX 3 Action is a 7B world action model with open weights. Given camera
  images, the current state and an instruction, it predicts a chunk of actions and
  about two seconds of future video. Run it on a robot arm through LeRobot,
  or fine-tune it for your robot, simulator or game. See the
  [FLUX 3 Action overview](/flux_3/flux3_action_overview).

  ## Start in ten seconds

  FLUX 3 is asynchronous. You **submit** a request and get back an `id` and a
  `polling_url`; then you **poll** that URL until the job turns `Ready` and returns
  your result. Here is the full round trip, text to video with audio.

  <RequestLifecycle />

  <Warning>
    Result URLs are signed and expire about 2 hours after the job finishes.
    Download the video promptly once the status is `Ready`.
  </Warning>

  ## Getting started

  <CardGroup cols={2}>
    <Card title="Quickstart" icon="rocket" href="/quick_start/get_started">
      Create an account, add credits, and make your first FLUX 3 call.
    </Card>

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

    <Card title="Video" icon="film" href="/flux_3/flux3_video">
      Generate from text, animate stills, pin keyframes, continue clips, all with 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, constraints.
    </Card>

    <Card title="FLUX 3 Image" icon="images" href="/flux_3/flux3_image_overview">
      Generate and edit images, combine references, and place elements with boxes.
    </Card>

    <Card title="FLUX 3 Action" icon="robot" href="/flux_3/flux3_action_overview">
      Open weights that output actions: run them on a robot arm, fine-tune on your robot or game.
    </Card>
  </CardGroup>
</div>


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