---
format: typebulb/v1
name: Video Clip Crop
---

**code.tsx**

```tsx
import React, { useState, useRef, useEffect } from "react";
import { createRoot } from "react-dom/client";
import { FFmpeg } from "@ffmpeg/ffmpeg";
import { fetchFile } from "@ffmpeg/util";

interface CropRegion { x: number; y: number; w: number; h: number }
type HandleType = "move" | "nw" | "ne" | "sw" | "se";

const clamp = (v: number, min: number, max: number) => Math.min(Math.max(v, min), max);
const toEven = (n: number) => Math.floor(n / 2) * 2;

const ASPECT_PRESETS: { label: string; ratio: number | null }[] = [
  { label: "Free", ratio: null },
  { label: "1:1", ratio: 1 },
  { label: "16:9", ratio: 16 / 9 },
  { label: "9:16", ratio: 9 / 16 },
  { label: "4:3", ratio: 4 / 3 },
  { label: "3:4", ratio: 3 / 4 },
];

function centeredCrop(ratio: number | null, vw: number, vh: number): CropRegion {
  if (!ratio || !vw || !vh) return { x: 0, y: 0, w: 1, h: 1 };
  const videoAR = vw / vh;
  let w: number, h: number;
  if (ratio >= videoAR) { w = 1; h = vw / (ratio * vh); }
  else { h = 1; w = (ratio * vh) / vw; }
  return { x: (1 - w) / 2, y: (1 - h) / 2, w, h };
}

// --- Crop Overlay ---
const CropOverlay = ({
  crop, onChange, containerRef, vw, vh, ar,
}: {
  crop: CropRegion;
  onChange: (r: CropRegion) => void;
  containerRef: React.RefObject<HTMLDivElement | null>;
  vw: number; vh: number; ar: number | null;
}) => {
  const dragRef = useRef<{ type: HandleType; mx: number; my: number; start: CropRegion } | null>(null);
  const [, tick] = useState(0);
  const arRef = useRef(ar); arRef.current = ar;
  const vwRef = useRef(vw); vwRef.current = vw;
  const vhRef = useRef(vh); vhRef.current = vh;
  const onChangeRef = useRef(onChange); onChangeRef.current = onChange;

  useEffect(() => {
    const getRect = () => containerRef.current?.getBoundingClientRect();

    const move = (cx: number, cy: number) => {
      const d = dragRef.current;
      if (!d) return;
      const rect = getRect();
      if (!rect) return;
      const { type, start } = d;
      const { x, y, w, h } = start;
      const right = x + w, bottom = y + h;

      if (type === "move") {
        const dx = (cx - d.mx) / rect.width;
        const dy = (cy - d.my) / rect.height;
        onChangeRef.current({ x: clamp(x + dx, 0, 1 - w), y: clamp(y + dy, 0, 1 - h), w, h });
        return;
      }

      const mouseX = clamp((cx - rect.left) / rect.width, 0, 1);
      const mouseY = clamp((cy - rect.top) / rect.height, 0, 1);
      let nx: number, ny: number, nw: number, nh: number;

      switch (type) {
        case "se": nx = x; ny = y; nw = mouseX - x; nh = mouseY - y; break;
        case "sw": nw = right - mouseX; nh = mouseY - y; nx = right - nw; ny = y; break;
        case "ne": nw = mouseX - x; nh = bottom - mouseY; nx = x; ny = bottom - nh; break;
        default:   nw = right - mouseX; nh = bottom - mouseY; nx = right - nw; ny = bottom - nh; break;
      }
      nw = Math.max(0.05, nw); nh = Math.max(0.05, nh);

      const _ar = arRef.current, _vw = vwRef.current, _vh = vhRef.current;
      if (_ar && _vw > 0 && _vh > 0) {
        const cur = (nw * _vw) / (nh * _vh);
        if (cur > _ar) { nw = (nh * _ar * _vh) / _vw; }
        else { nh = (nw * _vw) / (_ar * _vh); }
        switch (type) {
          case "sw": nx = right - nw; break;
          case "ne": ny = bottom - nh; break;
          case "nw": nx = right - nw; ny = bottom - nh; break;
        }
      }

      nx = clamp(nx, 0, 1); ny = clamp(ny, 0, 1);
      nw = clamp(nw, 0.05, 1 - nx); nh = clamp(nh, 0.05, 1 - ny);
      onChangeRef.current({ x: nx, y: ny, w: nw, h: nh });
    };

    const onMM = (e: MouseEvent) => move(e.clientX, e.clientY);
    const onTM = (e: TouchEvent) => { if (dragRef.current) e.preventDefault(); move(e.touches[0].clientX, e.touches[0].clientY); };
    const onEnd = () => { dragRef.current = null; tick(n => n + 1); };

    document.addEventListener("mousemove", onMM);
    document.addEventListener("mouseup", onEnd);
    document.addEventListener("touchmove", onTM, { passive: false });
    document.addEventListener("touchend", onEnd);
    return () => {
      document.removeEventListener("mousemove", onMM);
      document.removeEventListener("mouseup", onEnd);
      document.removeEventListener("touchmove", onTM);
      document.removeEventListener("touchend", onEnd);
    };
  }, []);

  const begin = (type: HandleType, e: React.MouseEvent | React.TouchEvent) => {
    e.preventDefault(); e.stopPropagation();
    const cx = "touches" in e ? e.touches[0].clientX : e.clientX;
    const cy = "touches" in e ? e.touches[0].clientY : e.clientY;
    dragRef.current = { type, mx: cx, my: cy, start: { ...crop } };
    tick(n => n + 1);
  };

  const { x, y, w, h } = crop;
  const active = dragRef.current !== null;
  const handles: { id: HandleType; pos: React.CSSProperties }[] = [
    { id: "nw", pos: { left: -6, top: -6, cursor: "nwse-resize" } },
    { id: "ne", pos: { right: -6, top: -6, cursor: "nesw-resize" } },
    { id: "sw", pos: { left: -6, bottom: -6, cursor: "nesw-resize" } },
    { id: "se", pos: { right: -6, bottom: -6, cursor: "nwse-resize" } },
  ];

  return (
    <div className="crop-overlay">
      <div className="crop-region"
        style={{
          left: `${x * 100}%`, top: `${y * 100}%`,
          width: `${w * 100}%`, height: `${h * 100}%`,
          cursor: active && dragRef.current?.type === "move" ? "grabbing" : "grab",
        }}
        onMouseDown={e => begin("move", e)}
        onTouchStart={e => begin("move", e)}
      >
        <div className="crop-grid">
          {[1, 2].map(i => (
            <React.Fragment key={i}>
              <div className="crop-grid-v" style={{ left: `${(i * 100) / 3}%` }} />
              <div className="crop-grid-h" style={{ top: `${(i * 100) / 3}%` }} />
            </React.Fragment>
          ))}
        </div>
        {handles.map(({ id, pos }) => (
          <div key={id} className="crop-handle" style={pos}
            onMouseDown={e => begin(id, e)} onTouchStart={e => begin(id, e)} />
        ))}
      </div>
    </div>
  );
};

// --- Range Control ---
const RangeControl = ({ label, value, max, onChange }: {
  label: string; value: number; max: number; onChange: (v: number) => void;
}) => (
  <div style={{ marginBottom: "1rem" }}>
    <div style={{ display: "flex", justifyContent: "space-between", marginBottom: "0.5rem", fontSize: "0.85rem" }}>
      <span style={{ fontWeight: 600 }}>{label}</span>
      <span style={{ color: "var(--text-muted)" }}>{value.toFixed(2)}s</span>
    </div>
    <input type="range" min={0} max={max || 100} step={0.01} value={value}
      onChange={e => onChange(parseFloat(e.target.value))}
      style={{ width: "100%", accentColor: "var(--accent)" }} />
  </div>
);

// --- App ---
const App = () => {
  const [loaded, setLoaded] = useState(false);
  const [loading, setLoading] = useState(true);
  const [processing, setProcessing] = useState(false);
  const [log, setLog] = useState<string[]>(["Initializing..."]);
  const [inputFile, setInputFile] = useState<File | null>(null);
  const [inputUrl, setInputUrl] = useState<string | null>(null);
  const [outputUrl, setOutputUrl] = useState<string | null>(null);
  const [duration, setDuration] = useState(0);
  const [startTime, setStartTime] = useState(0);
  const [endTime, setEndTime] = useState(3);
  const [cropEnabled, setCropEnabled] = useState(false);
  const [crop, setCrop] = useState<CropRegion>({ x: 0, y: 0, w: 1, h: 1 });
  const [aspectIdx, setAspectIdx] = useState(0);
  const [vidSize, setVidSize] = useState({ w: 0, h: 0 });
  const [includeAudio, setIncludeAudio] = useState(true);
  const [cap1080, setCap1080] = useState(false);

  const ffmpegRef = useRef(new FFmpeg());
  const videoRef = useRef<HTMLVideoElement>(null);
  const containerRef = useRef<HTMLDivElement>(null);

  const addLog = (msg: string) => setLog(prev => [...prev, msg]);
  const curAR = ASPECT_PRESETS[aspectIdx].ratio;

  useEffect(() => {
    const load = async () => {
      const ffmpeg = ffmpegRef.current;
      ffmpeg.on("log", ({ message }) => addLog(message));
      try {
        addLog("Loading FFmpeg WASM...");
        await ffmpeg.load({
          coreURL: tb.proxy("https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm/ffmpeg-core.js"),
          wasmURL: tb.proxy("https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm/ffmpeg-core.wasm"),
          classWorkerURL: tb.proxy("https://unpkg.com/@ffmpeg/ffmpeg@0.12.15/dist/esm/worker.js"),
        });
        addLog("FFmpeg loaded successfully!");
        setLoaded(true);
      } catch (err) {
        addLog(`Error loading FFmpeg: ${err}`);
      }
      setLoading(false);
    };
    load();
  }, []);

  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0] ?? null;
    setInputFile(file);
    if (file) { addLog(`Selected: ${file.name}`); setInputUrl(URL.createObjectURL(file)); }
    setOutputUrl(null);
    setCropEnabled(false);
    setCrop({ x: 0, y: 0, w: 1, h: 1 });
    setAspectIdx(0);
  };

  const handleMeta = (e: React.SyntheticEvent<HTMLVideoElement>) => {
    const v = e.currentTarget;
    setDuration(v.duration);
    setEndTime(Math.min(v.duration, 3));
    setVidSize({ w: v.videoWidth, h: v.videoHeight });
  };

  const handleSeek = (time: number, isStart: boolean) => {
    if (isStart) { setStartTime(time); if (time > endTime) setEndTime(time); }
    else { setEndTime(time); if (time < startTime) setStartTime(time); }
    if (videoRef.current) videoRef.current.currentTime = time;
  };

  const selectAspect = (i: number) => {
    setAspectIdx(i);
    setCrop(centeredCrop(ASPECT_PRESETS[i].ratio, vidSize.w, vidSize.h));
  };

  const handleClip = async () => {
    if (!inputFile || !loaded) return;
    setProcessing(true);
    addLog("Processing...");
    const ffmpeg = ffmpegRef.current;
    try {
      await ffmpeg.writeFile("input.mp4", await fetchFile(inputFile));
      const args = ["-ss", startTime.toString(), "-i", "input.mp4", "-to", (endTime - startTime).toString()];

      const filters: string[] = [];
      if (cropEnabled && vidSize.w > 0 && vidSize.h > 0) {
        let cw = toEven(Math.round(crop.w * vidSize.w));
        let ch = toEven(Math.round(crop.h * vidSize.h));
        let cx = toEven(Math.round(crop.x * vidSize.w));
        let cy = toEven(Math.round(crop.y * vidSize.h));
        if (cx + cw > vidSize.w) cw = toEven(vidSize.w - cx);
        if (cy + ch > vidSize.h) ch = toEven(vidSize.h - cy);
        cw = Math.max(cw, 2); ch = Math.max(ch, 2);
        filters.push(`crop=${cw}:${ch}:${cx}:${cy}`);
        addLog(`Crop: ${cw}×${ch} at (${cx},${cy})`);
      }
      if (cap1080) {
        filters.push("scale='if(gt(iw,ih),min(1080,iw),-2)':'if(gt(iw,ih),-2,min(1080,ih))'");
      }
      filters.push("scale=trunc(iw/2)*2:trunc(ih/2)*2");
      args.push("-vf", filters.join(","));

      args.push("-c:v", "libx264", "-preset", "ultrafast", "-crf", "18",
        "-profile:v", "baseline", "-level", "3.1", "-pix_fmt", "yuv420p");
      if (includeAudio) {
        args.push("-c:a", "aac", "-b:a", "192k");
      } else {
        args.push("-an");
      }
      args.push("-avoid_negative_ts", "make_zero",
        "-movflags", "+faststart", "output.mp4");

      addLog(`Re-encoding${cropEnabled ? " with crop" : ""} for frame-accurate cut...`);
      await ffmpeg.exec(args);
      const data = await ffmpeg.readFile("output.mp4");
      setOutputUrl(URL.createObjectURL(new Blob([data], { type: "video/mp4" })));
      addLog(`Done! ${startTime.toFixed(2)}s–${endTime.toFixed(2)}s${cropEnabled ? " (cropped)" : ""}`);
    } catch (err) {
      addLog(`Error: ${err}`);
    }
    setProcessing(false);
  };

  const handleDownload = async () => {
    if (!outputUrl) return;
    if (navigator.share && navigator.canShare?.({ files: [new File([], "t.mp4", { type: "video/mp4" })] })) {
      const resp = await fetch(outputUrl);
      const blob = await resp.blob();
      await navigator.share({ files: [new File([blob], "clipped.mp4", { type: "video/mp4" })] });
    } else {
      const a = document.createElement("a");
      a.href = outputUrl;
      a.download = "clipped.mp4";
      a.click();
    }
  };

  const pw = vidSize.w ? toEven(Math.round(crop.w * vidSize.w)) : 0;
  const ph = vidSize.h ? toEven(Math.round(crop.h * vidSize.h)) : 0;
  const ua = navigator.userAgent;
  const isWebView = (/iPhone|iPad|iPod/i.test(ua) && !ua.includes('Safari')) || /; wv\)/.test(ua);

  if (isWebView) return (
    <div className="container animate-in">
      <div className="in-app-banner">
        Your in-app browser can't download videos. Please open this page directly in your browser.
        <button className="copy-link-btn" onClick={async () => tb.copy(await tb.url())}>
          Copy Link
        </button>
      </div>
    </div>
  );

  return (
    <div className="container animate-in">
      <header className="main-header">
        <h1>Video Clip Crop</h1>
        <p className="subtitle">Clip and crop your videos locally in the browser. Nothing leaves your device.</p>
      </header>

      <div className="upload-section">
        <label htmlFor="file-upload"
          className={`upload-btn ${inputFile ? "has-file" : ""} ${loading ? "disabled" : ""}`}>
          {loading ? <span className="shimmer">Loading FFmpeg...</span> : inputFile ? `Selected: ${inputFile.name}` : "Choose Video File"}
          <input id="file-upload" type="file" accept="video/*" onChange={handleFileChange} disabled={loading} />
        </label>

      </div>

      {inputUrl && (
        <div className="page-card animate-in" style={{ cursor: "default", marginBottom: "2rem" }}>
          <div className="section-label">PREVIEW & EDIT</div>

          <div ref={containerRef} className="video-container">
            <video ref={videoRef} src={inputUrl} onLoadedMetadata={handleMeta} controls
              style={{ width: "100%", display: "block", background: "#000" }} />
            {cropEnabled && (
              <CropOverlay crop={crop} onChange={setCrop} containerRef={containerRef}
                vw={vidSize.w} vh={vidSize.h} ar={curAR} />
            )}
          </div>

          <div className="crop-section">
            <div className="crop-toggle-row">
              <button className={`toggle-btn ${cropEnabled ? "active" : ""}`}
                onClick={() => setCropEnabled(p => !p)}>
                <span className="toggle-dot" /> Crop
              </button>
              <button className={`toggle-btn ${includeAudio ? "active" : ""}`}
                onClick={() => setIncludeAudio(p => !p)}>
                <span className="toggle-dot" /> Audio
              </button>
              <button className={`toggle-btn ${cap1080 ? "active" : ""}`}
                onClick={() => setCap1080(p => !p)}>
                <span className="toggle-dot" /> Cap 1080p
              </button>
              {cropEnabled && pw > 0 && <span className="crop-dims">{pw} × {ph} px</span>}
            </div>
            {cropEnabled && (
              <div className="aspect-row animate-in">
                {ASPECT_PRESETS.map((p, i) => (
                  <button key={p.label} className={`aspect-btn ${aspectIdx === i ? "active" : ""}`}
                    onClick={() => selectAspect(i)}>{p.label}</button>
                ))}
              </div>
            )}
          </div>

          <div className="range-controls">
            <RangeControl label="Start" value={startTime} max={duration} onChange={v => handleSeek(v, true)} />
            <RangeControl label="End" value={endTime} max={duration} onChange={v => handleSeek(v, false)} />
          </div>

          <div className="toolbar" style={{ position: "relative", top: 0, marginTop: "1rem", boxShadow: "none", border: "none", background: "transparent" }}>
            <button className="btn" onClick={handleClip}
              disabled={!loaded || !inputFile || processing} style={{ width: "100%" }}>
              {processing ? <span className="shimmer">Processing...</span> : `${cropEnabled ? "Crop & " : ""}Clip ${startTime.toFixed(1)}s – ${endTime.toFixed(1)}s`}
            </button>
          </div>
        </div>
      )}

      {outputUrl && (
        <div className="page-card animate-in success-card" style={{ cursor: "default" }}>
          <div className="card-label">SUCCESS • READY TO DOWNLOAD</div>
          <video controls src={outputUrl} style={{ width: "100%", borderRadius: "8px", background: "#000" }} />
          <div style={{ marginTop: "1rem", display: "flex", justifyContent: "center" }}>
            <button className="btn" onClick={handleDownload} style={{ width: "100%" }}>
              Download Video
            </button>
          </div>
        </div>
      )}

      <div className="log-container">
        <div className="log-title">System Logs</div>
        {processing && log.length > 0 && (
          <div className="last-log">{log[log.length - 1]}</div>
        )}
        <details>
          <summary>View all logs ({log.length})</summary>
          <div className="log">
            {log.map((line, i) => <div key={i} style={{ marginBottom: "2px" }}>{line}</div>)}
          </div>
        </details>
      </div>
    </div>
  );
};

const container = document.getElementById("root");
const root = createRoot(container!);
root.render(<App />);
```
**styles.css**

```css
:root {
  --bg: #fafafa;
  --text: #1d1d1f;
  --text-muted: #6e6e73;
  --primary-action: #f5f5f5;
  --border: rgba(0, 0, 0, 0.08);
  --card-bg: #ffffff;
  --selection: rgba(0, 0, 0, 0.05);
  --button-bg: #1d1d1f;
  --button-text: #ffffff;
  --radius: 16px;
  --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.06);
  --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.08);
  --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.12);
  --accent: #1d1d1f;
}

html[data-theme="dark"] {
  --bg: #1c1c1c;
  --text: #f5f5f7;
  --text-muted: #a1a1a6;
  --primary-action: #3a3a3c;
  --border: rgba(255, 255, 255, 0.1);
  --card-bg: #232323;
  --selection: rgba(255, 255, 255, 0.08);
  --button-bg: #f5f5f7;
  --button-text: #1d1d1f;
  --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.4);
  --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.5);
  --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.6);
  --accent: #f5f5f7;
}

body {
  background-color: var(--bg);
  color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", "SF Pro Display", sans-serif;
  margin: 0;
  padding: 0;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  letter-spacing: -0.01em;
  line-height: 1.5;
}

.container {
  max-width: 640px;
  margin: 0 auto;
  padding: 3rem 2rem;
  min-height: 100vh;
}

.main-header {
  margin-bottom: 1.5rem;
  text-align: center;
}

h1 {
  font-size: 2.5rem;
  font-weight: 700;
  margin: 0 0 0.5rem 0;
  letter-spacing: -0.03em;
  line-height: 1.1;
}

.subtitle {
  color: var(--text-muted);
  font-size: 1rem;
  font-weight: 400;
  line-height: 1.6;
  max-width: 480px;
  margin: 0 auto;
}

.upload-section {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.25rem;
  margin-bottom: 2.5rem;
}

.upload-btn {
  background: var(--card-bg);
  color: var(--text);
  border: 1px solid var(--border);
  padding: 1rem 2rem;
  border-radius: 14px;
  cursor: pointer;
  font-weight: 600;
  font-size: 0.9375rem;
  transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
  box-shadow: var(--shadow-sm);
  letter-spacing: -0.01em;
}

.upload-btn:hover {
  box-shadow: var(--shadow-md);
}

.upload-btn.has-file {
  background: var(--card-bg);
  color: var(--text);
}

.upload-btn input {
  display: none;
}

.upload-btn.disabled {
  opacity: 0.5;
  cursor: not-allowed;
  transform: none !important;
}

@keyframes shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

.section-label {
  font-weight: 600;
  margin-bottom: 1rem;
  font-size: 0.9rem;
  color: var(--text-muted);
}

.toolbar {
  display: flex;
  gap: 0.5rem;
  justify-content: center;
}

.btn {
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  padding: 0.875rem 1.75rem;
  border-radius: 12px;
  font-weight: 600;
  font-size: 0.9375rem;
  cursor: pointer;
  transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
  box-shadow: var(--shadow-sm);
  letter-spacing: -0.01em;
}

.btn:hover {
  box-shadow: var(--shadow-md);
}

.btn:disabled {
  cursor: not-allowed;
  transform: none !important;
  box-shadow: var(--shadow-sm) !important;
}

.last-log {
  font-size: 0.8rem;
  color: var(--text-muted);
  font-family: "SF Mono", "Monaco", "Consolas", monospace;
  margin-bottom: 0.75rem;
}

.shimmer {
  background: linear-gradient(90deg, var(--text-muted) 25%, var(--text) 50%, var(--text-muted) 75%);
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
}

.page-card {
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1.75rem;
  position: relative;
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  box-shadow: var(--shadow-md);
}

.success-card {
  box-shadow: var(--shadow-lg);
}

.card-label {
  font-weight: 600;
  margin-bottom: 1.25rem;
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  color: var(--text-muted);
  text-transform: uppercase;
}

.range-controls {
  margin-top: 1.5rem;
}

input[type="range"] {
  -webkit-appearance: none;
  height: 4px;
  background: var(--selection);
  border-radius: 3px;
  outline: none;
  transition: background 0.2s;
}

input[type="range"]:hover {
  background: var(--border);
}

input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 16px;
  height: 16px;
  background: var(--text);
  border-radius: 50%;
  cursor: pointer;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
  transition: all 0.2s;
}

input[type="range"]::-webkit-slider-thumb:hover {
  transform: scale(1.15);
}

input[type="range"]::-moz-range-thumb {
  width: 16px;
  height: 16px;
  background: var(--text);
  border-radius: 50%;
  cursor: pointer;
  border: none;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
  transition: all 0.2s;
}

input[type="range"]::-moz-range-thumb:hover {
  transform: scale(1.15);
}

.log-container {
  margin-top: 3rem;
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 1rem;
  background: var(--card-bg);
  transition: all 0.2s;
}

.log-container:hover {
  border-color: var(--text-muted);
}

.log-title {
  font-weight: 600;
  font-size: 0.8125rem;
  color: var(--text-muted);
  letter-spacing: 0.01em;
  margin-bottom: 0.75rem;
}

.log-container summary {
  cursor: pointer;
  color: var(--text-muted);
  font-size: 0.75rem;
  font-weight: 500;
  letter-spacing: 0.01em;
  user-select: none;
}

.log {
  background: var(--selection) !important;
  padding: 1rem !important;
  border-radius: 8px !important;
  margin-top: 0.75rem !important;
  max-height: 200px !important;
  overflow-y: auto !important;
  font-family: "SF Mono", "Monaco", "Consolas", monospace !important;
  font-size: 0.75rem !important;
  line-height: 1.6 !important;
}

/* ============================================
   Video Container & Crop Overlay
   ============================================ */

.video-container {
  position: relative;
  border-radius: 8px;
  overflow: hidden;
  line-height: 0;
}

.crop-overlay {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 10;
  user-select: none;
  -webkit-user-select: none;
}

.crop-region {
  position: absolute;
  box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.55);
  border: 2px solid rgba(255, 255, 255, 0.85);
  pointer-events: auto;
  box-sizing: border-box;
  user-select: none;
  -webkit-user-select: none;
}

.crop-grid {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.crop-grid-v {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 1px;
  background: rgba(255, 255, 255, 0.25);
}

.crop-grid-h {
  position: absolute;
  left: 0;
  right: 0;
  height: 1px;
  background: rgba(255, 255, 255, 0.25);
}

.crop-handle {
  position: absolute;
  width: 14px;
  height: 14px;
  background: white;
  border-radius: 2px;
  pointer-events: auto;
  z-index: 20;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
  transition: transform 0.15s;
}

.crop-handle:hover {
  transform: scale(1.2);
}

/* ============================================
   Crop Controls
   ============================================ */

.crop-section {
  margin-top: 1.25rem;
  margin-bottom: 0.25rem;
}

.crop-toggle-row {
  display: flex;
  align-items: center;
  gap: 1rem;
  margin-bottom: 0.75rem;
}

.toggle-btn {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  background: var(--selection);
  border: 1px solid var(--border);
  color: var(--text-muted);
  padding: 0.5rem 1rem;
  border-radius: 10px;
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s;
  font-family: inherit;
}

.toggle-btn:hover {
  border-color: var(--text-muted);
}

.toggle-btn.active {
  background: var(--button-bg);
  color: var(--button-text);
  border-color: var(--button-bg);
}

.toggle-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: currentColor;
  opacity: 0.5;
  transition: all 0.2s;
}

.toggle-btn.active .toggle-dot {
  opacity: 1;
  box-shadow: 0 0 6px currentColor;
}

.crop-dims {
  font-size: 0.8rem;
  color: var(--text-muted);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
}

.aspect-row {
  display: flex;
  gap: 0.375rem;
  flex-wrap: wrap;
  margin-bottom: 0.5rem;
}

.aspect-btn {
  background: var(--selection);
  border: 1px solid var(--border);
  color: var(--text-muted);
  padding: 0.375rem 0.75rem;
  border-radius: 8px;
  font-size: 0.8rem;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s;
  font-family: inherit;
}

.aspect-btn:hover {
  border-color: var(--text-muted);
  color: var(--text);
}

.aspect-btn.active {
  background: var(--text);
  color: var(--bg);
  border-color: var(--text);
}

/* ============================================
   Animations
   ============================================ */

.in-app-banner {
  background: var(--selection);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 1rem 1.25rem;
  margin-bottom: 1.5rem;
  font-size: 0.875rem;
  color: var(--text);
  line-height: 1.5;
  text-align: center;
}

.copy-link-btn {
  display: block;
  margin: 0.75rem auto 0;
  padding: 0.5rem 1rem;
  font-size: 0.8rem;
  font-weight: 600;
  cursor: pointer;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 8px;
  transition: all 0.2s;
}

.copy-link-btn:hover { box-shadow: var(--shadow-sm); }

@keyframes fadeIn {
  from { opacity: 0; transform: translateY(20px); }
  to { opacity: 1; transform: translateY(0); }
}

.animate-in {
  animation: fadeIn 0.5s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}

video {
  display: block;
}

@media (max-width: 640px) {
  .container {
    padding: 2rem 1.25rem;
  }

  h1 {
    font-size: 2rem;
  }

  .subtitle {
    font-size: 0.9375rem;
  }

  .page-card {
    padding: 1.25rem;
  }
}
```
**index.html**

```html
<div id="root"></div>
```
**config.json**

```json
{
  "dependencies": {
    "react": "^19.2.3",
    "react-dom": "^19.2.3",
    "@ffmpeg/ffmpeg": "^0.12.15",
    "@ffmpeg/util": "^0.12.2"
  },
  "description": "FFmpeg Video Clipper & Cropper"
}
```