@tikagent/video-editor-cli 1.5.0-preview.3 → 1.5.0-preview.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/browser.js +1 -1
- package/dist/lib/cli.js +1 -1
- package/dist/lib/processes.js +1 -1
- package/dist/lib/telemetry.js +1 -1
- package/dist/runtime/app.js +1 -1
- package/dist/runtime/cli/lib/browser.js +1 -1
- package/dist/runtime/cli/lib/processes.js +1 -1
- package/dist/runtime/cli/lib/telemetry.js +1 -1
- package/dist/runtime/export-pipeline.js +1 -1
- package/dist/runtime/optimized-export.js +1 -1
- package/dist/runtime/server.js +1 -1
- package/dist/runtime/webcodecs-export.js +1 -1
- package/package.json +1 -1
package/dist/runtime/app.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";const uiText=(e,t)=>FableCutI18n.text(e,t),uiAttr=(e,t)=>escapeHtml(uiText(e,t)),Timeline=FableCutTimeline,TRACKS=[],TRACK_SIZE_PRESETS={s:{thumbs:!1,h:{V3:26,V2:26,V1:26,A1:22,A2:22,A3:22,A4:22}},m:{thumbs:!0,h:{V3:36,V2:44,V1:44,A1:32,A2:32,A3:32,A4:32}},l:{thumbs:!0,h:{V3:44,V2:58,V1:58,A1:42,A2:42,A3:42,A4:42}}},MAX_SOURCE_CHANNELS=16,TRACK_SIZE_KEY="fablecut-track-size",LAST_TRANS_KEY={in:"fablecut-last-trans-in",out:"fablecut-last-trans-out"},DEFAULT_LAST_TRANS={type:"fade",duration:1},RULER_H=26,SNAP_PX=8,MIN_DUR=.05,ZOOM_MIN=1,ZOOM_MAX=300,TIMELINE_FIT_FILL=.95,PROJECT_KEY="fablecut-project",requestedProjectId=new URLSearchParams(location.search).get("project_id");let activeProjectId=(requestedProjectId??localStorage.getItem(PROJECT_KEY)??"default").trim().toLowerCase();function projectApi(e){const t=new URLSearchParams(location.search).get("exportJob");return e+(e.includes("?")?"&":"?")+"project="+encodeURIComponent(activeProjectId)+(t?"&exportJob="+encodeURIComponent(t):"")}null!==requestedProjectId||/^[a-z0-9][a-z0-9_-]{0,63}$/.test(activeProjectId)||(activeProjectId="default");const DEFAULT_PROPS={x:0,y:0,scale:1,rotation:0,opacity:1,volume:1,speed:1,brightness:100,contrast:100,saturation:100,hue:0,blur:0,grayscale:0,sepia:0,invert:0,temperature:0,tint:0,vignette:0,filterPreset:"none",fit:"contain",cropL:0,cropT:0,cropR:0,cropB:0,flipH:!1,flipV:!1,cornerRadius:0,blend:"normal",chromaKey:"",chromaTolerance:26,chromaSoftness:12,bgRemove:!1,shake:0,shakeSpeed:8,rgbSplit:0,grain:0,text:"Enter Text",fontSize:72,color:"#ffffff",color2:"",font:"Segoe UI",bold:!0,italic:!1,weight:0,align:"center",letterSpacing:0,lineHeight:1.2,uppercase:!1,textShadow:12,glow:0,glowColor:"",textAnim:"none",wordRate:.15,direction:"auto",strokeWidth:0,strokeColor:"#000000",bgColor:"#000000",bgOpacity:0,boxW:0,boxH:0,boxFit:!1,vAlign:"middle"},ANIMATABLE=["x","y","scale","rotation","opacity","volume","speed","brightness","contrast","saturation","hue","blur","grayscale","sepia","invert","temperature","tint","vignette","cornerRadius","shake","rgbSplit","grain","fontSize","letterSpacing","glow"],TRANSITIONS=["none","fade","slide-left","slide-right","slide-up","slide-down","zoom","wipe","wipe-right","wipe-up","wipe-down","iris","spin","blur","whip","glitch","pop"],TEXT_ANIMS=["none","typewriter","word-pop","word-slide","karaoke","letter-pop","wave","bounce","shake","clip-reveal","zoom-in","font-cut","rise-mask"],BLEND_MODES=["normal","multiply","screen","overlay","lighter","soft-light","hard-light","color-dodge","darken","lighten","difference"],FILTER_PRESETS={none:{},cinematic:{contrast:112,saturation:118,temperature:-10,vignette:28},"teal-orange":{contrast:115,saturation:125,temperature:-18,hue:-8,vignette:22},noir:{grayscale:100,contrast:128,brightness:96,vignette:45},vintage:{sepia:42,contrast:92,brightness:106,saturation:88,temperature:12,vignette:30},faded:{contrast:84,brightness:110,saturation:82},warm:{temperature:28,brightness:103,saturation:108},cold:{temperature:-28,saturation:104},pop:{saturation:152,contrast:116},dreamy:{brightness:109,saturation:112,blur:.6,temperature:8},retro:{saturation:130,hue:-6,contrast:106,sepia:15},"bw-soft":{grayscale:100,contrast:95,brightness:108},cyberpunk:{saturation:140,hue:12,contrast:118,temperature:-15,vignette:25},sunset:{temperature:24,tint:6,brightness:105,saturation:116,contrast:104,vignette:20},midnight:{temperature:-24,brightness:88,contrast:122,saturation:94,vignette:38}},SYSTEM_FONTS=["Segoe UI","Arial","Georgia","Impact","Courier New","Trebuchet MS","Verdana","Times New Roman","Comic Sans MS","Consolas"],FONT_CUT_DEFAULT=["SourceHanSansSC-Bold","Rubik-Black","阿里巴巴普惠体-粗体","阿里巴巴普惠体-中黑","Impact"],STYLE_RESET={color2:"",glow:0,glowColor:"",strokeWidth:0,bgColor:"#000000",bgOpacity:0,rotation:0,letterSpacing:0,uppercase:!1,italic:!1,textShadow:12,fontCutSet:void 0,align:"center"},TITLE_STYLES={plain:{label:"Plain",place:"center",props:{font:"Segoe UI",fontSize:72,bold:!0,color:"#ffffff",textAnim:"none"}},impact:{label:"Impact",place:"lower",props:{font:"SourceHanSansSC-Bold",fontSize:96,bold:!1,uppercase:!0,color:"#ffffff",textShadow:22,textAnim:"word-pop"}},elegant:{label:"Elegant",place:"center",props:{font:"Georgia",fontSize:88,bold:!1,color:"#ffffff",color2:"#ffd166",letterSpacing:2,textAnim:"clip-reveal"}},kinetic:{label:"Kinetic cut",place:"center",props:{font:"Rubik-Black",fontSize:120,bold:!1,uppercase:!0,color:"#ffd166",letterSpacing:3,textAnim:"font-cut",fontCutSet:["SourceHanSansSC-Bold","Rubik-Black","阿里巴巴普惠体-粗体","阿里巴巴普惠体-中黑"]}},neon:{label:"Neon",place:"center",props:{font:"阿里巴巴普惠体-中黑",fontSize:104,bold:!1,uppercase:!0,color:"#ffffff",glow:60,glowColor:"#22d3ee",textAnim:"wave"}},handwritten:{label:"Handwritten",place:"lower-left",props:{font:"Comic Sans MS",fontSize:92,bold:!1,color:"#ffffff",rotation:-4,textAnim:"word-slide"}},serifDrop:{label:"Serif drop",place:"center",props:{font:"Times New Roman",fontSize:96,bold:!1,color:"#ffffff",textShadow:18,textAnim:"zoom-in"}},subtitle:{label:"Subtitle",place:"lower",props:{font:"Roboto-Regular",fontSize:52,bold:!1,color:"#ffffff",bgColor:"#000000",bgOpacity:.5,textAnim:"karaoke"}},boldRise:{label:"Bold rise",place:"lower",props:{font:"阿里巴巴普惠体-粗体",fontSize:92,bold:!1,uppercase:!0,color:"#ffffff",textAnim:"rise-mask"}},luxury:{label:"Luxury",place:"center",props:{font:"Chocolate Classical Sans",fontSize:88,bold:!1,uppercase:!0,color:"#faf0dc",color2:"#c9a227",letterSpacing:6,textAnim:"clip-reveal"}}},STYLE_CYCLE=["impact","elegant","kinetic","neon","handwritten","serifDrop","boldRise","luxury"],AUDIO_EXT=/\.(mp3|wav|ogg|m4a|aac|flac|mpeg)$/i,VIDEO_EXT=/\.(mp4|webm|mov|mkv|m4v)$/i,IMAGE_EXT=/\.(png|jpe?g|gif|webp|avif)$/i,ASPECT_PRESETS=[{label:"16:9 · 1280×720",w:1280,h:720},{label:"16:9 · 1920×1080",w:1920,h:1080},{label:"9:16 · Reel 1080×1920",w:1080,h:1920},{label:"4:5 · IG 1080×1350",w:1080,h:1350},{label:"1:1 · 1080×1080",w:1080,h:1080}],FPS_PRESETS=[24,25,30,50,60],WAVE_PEAKS_PER_SEC=50,TRACK_IDS=new Set(TRACKS.map(e=>e.id)),AUDIO_TRACK_IDS=[];function ensureTrack(e){if(!project.tracks.some(t=>t.id===e)){const t="A"===e[0]?"audio":"video";let n=project.tracks.findIndex(n=>"video"===t&&("audio"===n.kind||+n.id.slice(1)<+e.slice(1)));n<0&&(n=project.tracks.length),project.tracks.splice(n,0,{id:e,kind:t})}return refreshTrackRegistry(),e}function ensureAudioTrackCount(e){const t=AUDIO_TRACK_IDS.length;for(let t=1;t<=Math.min(e,16);t++)ensureTrack("A"+t);return AUDIO_TRACK_IDS.length-t}function visibleTracks(){const e=new Set(project.clips.map(e=>e.track));return TRACKS.filter(t=>e.has(t.id))}function visibleTrackSignature(){return visibleTracks().map(e=>`${e.id}:${e.h}:${e.name||""}`).join(",")}function refreshTrackRegistry(){const e={video:["#4f8cff","#7b6cff","#ffd166"],audio:["#7ec249","#5a9e3a","#4a8a2f","#3a7226"]},t=TRACK_SIZE_PRESETS[state.trackSize]||TRACK_SIZE_PRESETS.s;TRACKS.splice(0,TRACKS.length,...(project.tracks||[]).map(n=>({...n,h:t.h[n.id]??t.h["audio"===n.kind?"A1":"V1"],color:e[n.kind][(+n.id.slice(1)-1)%e[n.kind].length]}))),TRACK_IDS.clear();for(const e of TRACKS)TRACK_IDS.add(e.id);if(AUDIO_TRACK_IDS.splice(0,AUDIO_TRACK_IDS.length,...TRACKS.filter(e=>"audio"===e.kind).map(e=>e.id)),runtime.audio){const e=runtime.audio,t=visibleTracks().filter(e=>"audio"===e.kind).map(e=>e.id),n=Object.keys(e.trackBus).filter(e=>!t.includes(e));for(const t of n)e.trackBus[t].disconnect(),delete e.trackBus[t];const i=t.filter(t=>!e.trackBus[t]);e.audioTrackIds=Object.keys(e.trackBus),(i.length||n.length)&&addLiveAudioTrackBuses(i)}}function syncTimeline(){Timeline.normalize(project),refreshTrackRegistry(),state.disabledTracks=new Set(project.disabledTracks||[]);const e=visibleTrackSignature();els.tracks.dataset.signature!==e&&(buildTrackDOM(),buildMeterDOM(),localStorage.getItem(TL_H_KEY)||setTimelineHeight(defaultTimelineHeight())),syncAllTrackDisabledUI();for(const e of project.clips)routeClipGain(e)}function timelineSnapshot(){return JSON.stringify({clips:project.clips,tracks:project.tracks,disabledTracks:project.disabledTracks,cover:project.cover})}function restoreTimeline(e){const t=JSON.parse(e),n=Array.isArray(t)?{clips:t}:t,i=new Map(project.clips.map(e=>[e.id,e]));n.clips=n.clips.map(e=>{const t=i.get(e.id);if(!t)return e;for(const n of Object.keys(t))Object.hasOwn(e,n)||delete t[n];return Object.assign(t,e)}),Object.assign(project,n)}function addLiveAudioTrackBuses(e){const t=runtime.audio;if(t){for(const n of e)t.trackBus[n]||(t.trackBus[n]=t.ctx.createGain());t.audioTrackIds=Object.keys(t.trackBus),t.meterVersion=(t.meterVersion||0)+1;for(const e of Object.values(t.trackBus))e.disconnect(),e.connect(t.master);for(const e of t.meters||[])e.port.onmessage=null,e.disconnect();t.meters=[],t.meterReady=!1,installMeterWorklet(t).catch(()=>{})}}const SETTINGS_KEY="fablecut-settings",DEFAULT_UI_VISIBILITY={showAssets:!1,showInspector:!1,showNewProject:!1,showConnectionStatus:!1,showSettings:!1,showCloseGap:!1,showFindGap:!1,showSnap:!1,showAudioHold:!1,showProjectSwitcher:!1,showLayoutControls:!1,showShortcuts:!1,showMonitorHeader:!1,showAudioMeter:!1,showBoundaryNavigation:!1,showTimelineFit:!0,showCover:!1},DEFAULT_SETTINGS={language:"zh-CN",exportEngine:"webcodecs",exportProfile:"delivery",linkSelect:!1,...DEFAULT_UI_VISIBILITY};let settings={...DEFAULT_SETTINGS};function loadSettings(){try{const e=JSON.parse(localStorage.getItem(SETTINGS_KEY)||"null");if(!e||"object"!=typeof e)return void(settings={...DEFAULT_SETTINGS});const t={...DEFAULT_SETTINGS};for(const n of Object.keys(DEFAULT_SETTINGS))Object.hasOwn(e,n)&&(t[n]=e[n]);t.language=FableCutI18n.normalize(t.language),t.linkSelect=!!t.linkSelect,["optimized","fast","webcodecs"].includes(t.exportEngine)||(t.exportEngine="webcodecs"),["delivery","auto","videotoolbox-h264","videotoolbox-hevc","nvenc-h264","nvenc-hevc"].includes(t.exportProfile)||(t.exportProfile="delivery");for(const e of Object.keys(DEFAULT_UI_VISIBILITY))"boolean"!=typeof t[e]&&(t[e]=DEFAULT_UI_VISIBILITY[e]);settings=t}catch{settings={...DEFAULT_SETTINGS}}}function saveSettings(){try{localStorage.setItem(SETTINGS_KEY,JSON.stringify(settings))}catch{}}function getSetting(e){return settings[e]}function setSetting(e,t){if(!Object.hasOwn(DEFAULT_SETTINGS,e))return;if("exportEngine"===e&&!["optimized","fast","webcodecs"].includes(t))return;const n=Object.hasOwn(DEFAULT_UI_VISIBILITY,e);n&&"boolean"!=typeof t||(settings[e]=t,saveSettings(),n&&applyInterfaceVisibility(),"showLayoutControls"!==e||t||setTrackSize("l"),"showCover"===e&&(!t&&$("coverDialog").open&&$("coverDialog").close(),refreshCoverButton()))}function applyInterfaceVisibility(){for(const e of document.querySelectorAll("[data-ui-setting]"))e.toggleAttribute("data-ui-visible",!0===getSetting(e.dataset.uiSetting));const e=[];getSetting("showAssets")&&e.push("minmax(0, var(--side-panel-w))"),e.push("minmax(0, 1fr)"),getSetting("showInspector")&&e.push("minmax(0, var(--side-panel-w))"),document.querySelector(".upper").style.gridTemplateColumns=e.join(" ");for(const e of document.querySelectorAll("[data-visibility-setting]"))e.checked=!0===getSetting(e.dataset.visibilitySetting)}const project={name:"Untitled Project",width:1280,height:720,fps:30,background:"#000000",revision:0,cover:null,folders:[],media:[],tracks:[],clips:[],markers:[],inPoint:null,outPoint:null,disabledTracks:[]},state={time:0,playing:!1,pps:60,snap:!0,previewRate:1,selId:null,selIds:new Set,trackSize:"l",connected:!1,exporting:!1,rendering:!1,guides:!1,viewZoom:1,audioHold:!1,ffmpeg:!1,ffprobe:!1,dirtyTimeline:!0,gesture:!1,clipGestureWidth:null,clipTrimOrigins:null,workAreaPlay:!1,binTab:"project",disabledTracks:new Set,transFocus:null,kfGraphs:new Set};function normalizeDisabledTracks(e){if(null==e)return[];const t=Array.isArray(e)?e:[];return[...new Set(t.filter(e=>TRACK_IDS.has(e)))].sort()}function isTrackEnabled(e){return!state.disabledTracks.has(e)}function syncTrackDisabledUI(e){const t=isTrackEnabled(e),n=els.trackHeaders.querySelector(`.track-head[data-track="${e}"]`),i=els.tracks.querySelector(`.track[data-track="${e}"]`);if(n){n.classList.toggle("disabled",!t),n.querySelector(".track-id").textContent=trackLabel(e);const i=n.querySelector(".track-toggle");i&&(i.setAttribute("aria-pressed",t?"true":"false"),i.title=`${trackLabel(e)} · ${uiText(t?"Disable track":"Enable track")}`,i.setAttribute("aria-label",i.title),i.innerHTML=trackToggleIcon(trackIconKind(e)))}i&&i.classList.toggle("disabled",!t)}function syncAllTrackDisabledUI(){for(const e of TRACKS)syncTrackDisabledUI(e.id)}function toggleTrackEnabled(e){TRACK_IDS.has(e)&&(state.disabledTracks.has(e)?state.disabledTracks.delete(e):state.disabledTracks.add(e),project.disabledTracks=[...state.disabledTracks].sort(),syncTrackDisabledUI(e),scheduleSave())}const runtime={clipEls:new Map,previewMedia:new Map,previewWait:null,previewRemainder:0,scrub:null,clipGain:new Map,mediaAux:new Map,audioBufs:new Map,wavePeaks:new Map,library:{},customFonts:[],fontsLoading:null,undo:[],redo:[],audio:null,saveTimer:null,savePromise:null,pendingSync:!1,sfxPreview:null,importFolderId:null,binDragFolderId:null,binCtxMenu:null},$=e=>document.getElementById(e),els={binList:$("binList"),binEmpty:$("binEmpty"),fileInput:$("fileInput"),binTabs:$("binTabs"),libList:$("libList"),toast:$("toast"),preview:$("preview"),tcCurrent:$("tcCurrent"),tcTotal:$("tcTotal"),btnPlay:$("btnPlay"),inspector:$("inspector"),trackHeaders:$("trackHeaders"),timelineScroll:$("timelineScroll"),tracksContent:$("tracksContent"),tracks:$("tracks"),playhead:$("playhead"),coverFrames:$("coverFrames"),ruler:$("ruler"),zoomSlider:$("zoomSlider"),btnSnap:$("btnSnap"),thumbProgress:$("thumbProgress"),thumbProgressBar:$("thumbProgressBar"),thumbProgressLabel:$("thumbProgressLabel"),btnAudioHold:$("btnAudioHold"),exportOverlay:$("exportOverlay"),exportProgress:$("exportProgress"),exportTitle:$("exportTitle"),exportNote:$("exportNote"),projectName:$("projectName"),projectSelect:$("projectSelect"),btnNewProject:$("btnNewProject"),currentProjectName:$("currentProjectName"),aspectSel:$("aspectSel"),fpsSel:$("fpsSel"),btnGuides:$("btnGuides"),btnZoom100:$("btnZoom100"),safeOverlay:$("safeOverlay"),btnSpeed:$("btnSpeed"),monitorStage:$("monitorStage"),monitorScroll:$("monitorScroll"),monitorZoomInner:$("monitorZoomInner"),kfGraphs:$("kfGraphs")},ctx2d=els.preview.getContext("2d"),uid=()=>Math.random().toString(36).slice(2,9),clamp=(e,t,n)=>Math.min(n,Math.max(t,e));function escapeHtml(e){return String(e??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}function fmt(e){e=Math.max(0,e);const t=Math.floor(e/60),n=Math.floor(e%60),i=Math.floor(e%1*project.fps),r=e=>String(e).padStart(2,"0");return`${r(t)}:${r(n)}:${r(i)}`}const getMedia=e=>project.media.find(t=>t.id===e),getClip=e=>project.clips.find(t=>t.id===e),clipEnd=e=>e.start+e.duration,trackOf=e=>TRACKS.find(t=>t.id===e.track);function projDur(){let e=0;for(const t of project.clips){if(!trackOf(t))continue;const n=clipEnd(t);Number.isFinite(n)&&n>e&&(e=n)}return e}const clipSpeed=e=>clamp(+e.props?.speed||1,.1,8),speedIntCache=new Map;function kfChannel(e,t,n,i){const r=e.keyframes?.[t];if(!Array.isArray(r)||!r.length)return i;if(n<=r[0].t)return r[0].v;if(n>=r[r.length-1].t)return r[r.length-1].v;for(let e=0;e<r.length-1;e++){const t=r[e],i=r[e+1];if(n>=t.t&&n<=i.t){const e=(n-t.t)/Math.max(1e-6,i.t-t.t),r=EASE[i.ease||"ease-in-out"]||EASE.linear;return t.v+(i.v-t.v)*r(e)}}return i}function hasSpeedRamp(e){return Array.isArray(e.keyframes?.speed)&&e.keyframes.speed.length>0}function mediaTimeAt(e,t){const n=clipSpeed(e),i=clamp(t-e.start,0,e.duration);if(!hasSpeedRamp(e))return e.in+i*n;const r=JSON.stringify(e.keyframes.speed)+"|"+e.duration.toFixed(4)+"|"+n;let a=speedIntCache.get(e.id);if(!a||a.key!==r){const t=1/120,i=Math.max(2,Math.ceil(e.duration/t)+1),o=new Float32Array(i);let s=clamp(kfChannel(e,"speed",0,n),.1,8);for(let r=1;r<i;r++){const i=Math.min(e.duration,r*t),a=clamp(kfChannel(e,"speed",i,n),.1,8);o[r]=o[r-1]+(s+a)/2*(i-(r-1)*t),s=a}a={key:r,cum:o,step:t},speedIntCache.set(e.id,a)}const o=Math.min(a.cum.length-1,i/a.step),s=Math.floor(o),l=o-s,c=s>=a.cum.length-1?a.cum[a.cum.length-1]:a.cum[s]+(a.cum[s+1]-a.cum[s])*l;return e.in+c}let toastTimer=null;function toast(e){els.toast&&(els.toast.textContent=e,els.toast.classList.add("show"),clearTimeout(toastTimer),toastTimer=setTimeout(()=>els.toast.classList.remove("show"),3200))}function isTypingTarget(e){if(!e||e===document.body||e===document.documentElement)return!1;const t=e.tagName;if("TEXTAREA"===t||"SELECT"===t)return!0;if("INPUT"===t){const t=(e.type||"text").toLowerCase();return!["range","checkbox","radio","button","submit","reset","color","file","hidden"].includes(t)}return!!e.isContentEditable}function updateProjectTitle(e=project.name){els.currentProjectName.textContent=e,els.currentProjectName.title=e}async function loadProjectCatalog(){const e=await fetch("/api/projects",{cache:"no-store",signal:AbortSignal.timeout(1e4)});if(!e.ok)throw new Error("projects unavailable");const t=await e.json();null!==requestedProjectId||runtime.projectLoaded||!t.length||t.some(e=>e.id===activeProjectId)||(activeProjectId=t.find(e=>"default"===e.id)?.id||t[0].id),localStorage.setItem(PROJECT_KEY,activeProjectId);const n=new URL(location.href);n.searchParams.set("project_id",activeProjectId),history.replaceState(null,"",n),els.projectSelect.replaceChildren(...t.map(e=>{const t=document.createElement("option");return t.value=e.id,t.textContent=e.name,t.selected=e.id===activeProjectId,t})),updateProjectTitle(t.find(e=>e.id===activeProjectId)?.name||project.name)}function openProject(e){localStorage.setItem(PROJECT_KEY,e);const t=new URL(location.href);t.searchParams.set("project_id",e),location.href=t.toString()}async function connectServer(){if(runtime.connecting)return;runtime.connecting=!0;const e=runtime.projectReadVersion=(runtime.projectReadVersion||0)+1;try{if(!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(activeProjectId))throw new Error(uiText("Invalid project link. Open the project again from its original entry."));await loadProjectCatalog();const t=await fetchProjectDocument();if(e!==runtime.projectReadVersion)return;applyProject(t),clearProjectLoadError(),new URLSearchParams(location.search).has("exportJob")||listenSSE(),fetch("/api/export/ffmpeg").then(e=>e.json()).then(e=>{state.ffmpeg=!!e.available,state.ffprobe=!!e.ffprobe}).catch(()=>{})}catch(t){if(e!==runtime.projectReadVersion)return;state.connected=!1,els.projectName.textContent=uiText("⚪ local session"),"file:"===location.protocol&&null===requestedProjectId||showProjectLoadError(t)}finally{runtime.connecting=!1}state.connected&&!new URLSearchParams(location.search).has("exportJob")&&await probeMissingMeta()}async function fetchProjectDocument(){const e=await fetch(projectApi("/api/project"),{cache:"no-store",signal:AbortSignal.timeout(1e4)});if(!e.ok){const t=new Error(uiText("Project request failed ({status})",{status:e.status}));throw t.missing=404===e.status,t}const t=await e.json();if(!t||!Array.isArray(t.clips)||!Array.isArray(t.media))throw new Error(uiText("Invalid project data"));const n=e=>e&&"object"==typeof e&&!Array.isArray(e);if(t.media.some(e=>!n(e)||"string"!=typeof e.id||!["video","audio","image","svg"].includes(e.kind)||null!=e.name&&"string"!=typeof e.name||null!=e.src&&"string"!=typeof e.src)||t.clips.some(e=>!n(e)||"string"!=typeof e.id||null!=e.props&&!n(e.props)||"text"===e.kind&&null!=e.props?.text&&"string"!=typeof e.props.text)||["markers","folders","disabledTracks"].some(e=>null!=t[e]&&!Array.isArray(t[e])))throw new Error(uiText("Invalid project data"));return t}function showProjectLoadError(e){runtime.projectEpoch=(runtime.projectEpoch||0)+1,runtime.projectLoadError=e,state.connected=!1,clearTimeout(runtime.saveTimer),runtime.saveTimer=null,pause();const t=uiText(e.missing?"Project not found":"Project could not be loaded");$("projectLoadTitle").textContent=t,$("projectLoadHelp").textContent=uiText(e.missing?"This project is not in the current or legacy storage folder. It may have been moved or deleted. Restore the project folder, then retry.":"The project file could not be read or the editor service is unavailable. Check the file or service, then retry."),$("projectLoadId").textContent=uiText("Project ID: {id}",{id:activeProjectId}),$("projectLoadDetails").textContent=e.message||"",runtime.projectLoaded||updateProjectTitle(t);const n=$("projectLoadDialog");n.open||n.showModal()}function clearProjectLoadError(){runtime.projectLoadError=null,runtime.projectLoaded=!0,state.connected=!0,els.projectName.textContent=uiText("🟢 connected"),$("projectLoadDialog").close()}els.projectSelect.addEventListener("change",()=>openProject(els.projectSelect.value)),els.btnNewProject.addEventListener("click",async()=>{const e=prompt(uiText("Project name"),uiText("Untitled Project"));if(null!=e&&e.trim())try{const t=await fetch("/api/projects",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:e.trim()})}),n=await t.json();if(!t.ok)throw new Error(n.error||"create failed");openProject(n.id)}catch(e){alert(uiText("Couldn't create project: ")+e.message)}}),$("projectLoadDialog").addEventListener("cancel",e=>e.preventDefault()),$("projectLoadDialog").addEventListener("keydown",e=>e.stopPropagation()),$("btnRetryProject").addEventListener("click",async()=>{const e=$("btnRetryProject");e.disabled=!0;try{await connectServer()}finally{e.disabled=!1}});const TIMELINE_START_TIME=0;function normalizeWorkArea(e,t,n=0){let i=null!=e&&isFinite(e)?Math.max(n,+e):null,r=null!=t&&isFinite(t)?Math.max(n,+t):null;return null!=i&&null!=r&&r<=i&&(i=null,r=null),{inPoint:i,outPoint:r}}function getFolder(e){return e?project.folders.find(t=>t.id===e):null}function normalizeFolders(e){if(!Array.isArray(e))return[];const t=[],n=new Set;for(const i of e)i&&i.id&&!n.has(i.id)&&(n.add(i.id),t.push({id:String(i.id),name:String(i.name||"Folder").trim()||"Folder",parentId:i.parentId||null,open:!1!==i.open}));for(const e of t)e.parentId&&!n.has(e.parentId)&&(e.parentId=null);return t}function normalizeMediaEntry(e){return e&&"object"==typeof e?{...e,name:e.name||e.id,folderId:e.folderId||null}:e}function folderChildren(e){return project.folders.filter(t=>(t.parentId||null)===(e||null)).sort((e,t)=>e.name.localeCompare(t.name,void 0,{sensitivity:"base"}))}function mediaInFolder(e){return project.media.filter(t=>(t.folderId||null)===(e||null)).sort((e,t)=>e.name.localeCompare(t.name,void 0,{sensitivity:"base"}))}function isSelfOrFolderAncestor(e,t){let n=getFolder(t);for(;n;){if(n.id===e)return!0;n=n.parentId?getFolder(n.parentId):null}return!1}function addFolder(e=null){e&&!getFolder(e)&&(e=null);const t={id:"f_"+uid(),name:"New folder",parentId:e||null,open:!0};if(project.folders.push(t),e){const t=getFolder(e);t&&(t.open=!0)}return scheduleSave(),renderBin(),requestAnimationFrame(()=>startFolderRename(t.id)),t}function deleteFolder(e){const t=getFolder(e);if(!t)return;const n=t.parentId||null,i=new Set,r=e=>{i.add(e);for(const t of project.folders)t.parentId===e&&r(t.id)};r(e);for(const e of project.media)e.folderId&&i.has(e.folderId)&&(e.folderId=n);for(const e of project.folders)e.parentId&&i.has(e.parentId)&&!i.has(e.id)&&(e.parentId=n);project.folders=project.folders.filter(e=>!i.has(e.id)),scheduleSave(),renderBin()}function moveMediaToFolder(e,t){const n=getMedia(e);if(n){if(t&&!getFolder(t)&&(t=null),n.folderId=t||null,t){const e=getFolder(t);e&&(e.open=!0)}scheduleSave(),renderBin()}}function moveFolderToParent(e,t){const n=getFolder(e);if(n&&(!t||t!==e&&!isSelfOrFolderAncestor(e,t))){if(t&&!getFolder(t)&&(t=null),n.parentId=t||null,t){const e=getFolder(t);e&&(e.open=!0)}scheduleSave(),renderBin()}}function startFolderRename(e){const t=els.binList.querySelector(`.bin-folder[data-folder-id="${e}"] .bin-folder-name`);if(!t)return;t.contentEditable="true",t.focus();const n=window.getSelection(),i=document.createRange();i.selectNodeContents(t),n.removeAllRanges(),n.addRange(i),t.addEventListener("blur",()=>{t.contentEditable="false";const n=getFolder(e);if(!n)return;const i=t.textContent.replace(/\s+/g," ").trim()||"Folder";n.name=i,t.textContent=i,scheduleSave()},{once:!0}),t.addEventListener("keydown",n=>{"Enter"===n.key&&(n.preventDefault(),t.blur()),"Escape"===n.key&&(n.preventDefault(),t.textContent=getFolder(e)?.name||"Folder",t.blur())})}function applyProject(e){$("coverDialog").open&&$("coverDialog").close(),runtime.projectEpoch=(runtime.projectEpoch||0)+1,runtime.timelineMenu&&closeTimelineMenu();const t=normalizeWorkArea((e=Timeline.normalize(structuredClone(e))).inPoint,e.outPoint),n=e.disabledTracks||[];Object.assign(project,{name:e.name||"Untitled Project",width:e.width||1280,height:e.height||720,fps:e.fps||30,background:e.background||"#000000",revision:e.revision||0,cover:e.cover||null,delivery:e.delivery||null,outputs:e.outputs||[],folders:normalizeFolders(e.folders),media:(e.media||[]).map(normalizeMediaEntry),tracks:e.tracks||[],clips:e.clips||[],markers:(e.markers||[]).filter(e=>e&&isFinite(e.t)).sort((e,t)=>e.t-t.t),inPoint:t.inPoint,outPoint:t.outPoint,disabledTracks:n});const i=[...els.projectSelect.options].find(e=>e.value===activeProjectId);i&&(i.textContent=project.name),updateProjectTitle();const r=new Set(project.folders.map(e=>e.id));for(const e of project.media)e.folderId&&!r.has(e.folderId)&&(e.folderId=null);state.disabledTracks=new Set(n);for(const e of project.clips){if(e.props={...DEFAULT_PROPS,...e.props||{}},"string"!=typeof e.name){const t=getMedia(e.mediaId)?.name;e.name=t?t.replace(/\.[^.]+$/,""):"text"===e.kind?"Text":"adjust"===e.kind?"Adjustment":"Clip"}if(e.keyframes)for(const t of Object.values(e.keyframes))Array.isArray(t)&&t.sort((e,t)=>e.t-t.t)}syncTimeline(),relinkClips(),state.audioHold?setAudioHold(!1):stopAudioHoldNodes(),resetPreviewWait();for(const e of[...runtime.clipEls.keys()])releaseClipEl(e);els.preview.width=project.width,els.preview.height=project.height,syncAspectSel(),syncFpsSel(),pruneSelection(),state.dirtyTimeline=!0,renderBin(),renderInspector(),updateWorkArea(),syncTrimIOButton(),syncAllTrackDisabledUI(),refreshCoverButton()}function scheduleSave(){runtime.projectLoadError||(refreshCoverButton(),runtime.timelineMenu&&closeTimelineMenu(),state.gesture||syncTimeline(),state.dirtyTimeline=!0,syncHistoryButtons(),state.connected&&(clearTimeout(runtime.saveTimer),runtime.saveTimer=setTimeout(()=>{runtime.saveTimer=null,saveProjectNow().catch(e=>toast(e.message))},400)))}async function saveProjectNow(){if(runtime.projectLoadError)throw new Error(uiText("Project could not be loaded"));for(;runtime.savePromise||state.gesture;)runtime.savePromise?await runtime.savePromise:await new Promise(e=>window.addEventListener("timelinegestureend",e,{once:!0}));if(runtime.projectLoadError)throw new Error(uiText("Project could not be loaded"));project.revision++;const e=JSON.stringify(projectJSON(),null,2),t=(async()=>{const t=await fetch(projectApi("/api/project"),{method:"PUT",headers:{"Content-Type":"application/json"},body:e});if(409===t.status)throw await syncFromServer(!0),new Error(uiText("Project was updated externally — your last change may need redoing."));if(!t.ok)throw new Error("Project save failed")})();runtime.savePromise=t;try{await t}finally{runtime.savePromise===t&&(runtime.savePromise=null)}}function projectJSON(){const{name:e,width:t,height:n,fps:i,background:r,revision:a,folders:o,media:s,clips:l,markers:c,inPoint:d,outPoint:u,disabledTracks:p}=project;return{name:e,width:t,height:n,fps:i,background:r,revision:a,cover:project.cover,...project.delivery?{delivery:project.delivery}:{},...project.outputs?.length?{outputs:project.outputs}:{},tracks:project.tracks.map(({id:e,kind:t})=>({id:e,kind:t})),folders:(o||[]).map(({id:e,name:t,parentId:n,open:i})=>({id:e,name:t,parentId:n||null,open:!1!==i})),media:s.filter(e=>!e.transient).map(({id:e,name:t,kind:n,src:i,duration:r,width:a,height:o,folderId:s,asrUrl:l,asrLocalPath:c})=>({id:e,name:t,kind:n,src:i,duration:r,width:a,height:o,folderId:s||null,...void 0===l?{}:{asrUrl:l},...void 0===c?{}:{asrLocalPath:c}})),clips:l.map(({id:e,mediaId:t,kind:n,track:i,start:r,in:a,duration:o,name:s,props:l,keyframes:c,transitionIn:d,transitionOut:u,linkedId:p,linkGroup:m})=>{const f={id:e,mediaId:t,kind:n,track:i,start:r,in:a,duration:o,name:s,props:l,keyframes:c,transitionIn:d,transitionOut:u};return m&&(f.linkGroup=m),p&&(f.linkedId=p),f}),markers:(c||[]).map(({t:e,label:t})=>t?{t:e,label:t}:{t:e}),inPoint:d??null,outPoint:u??null,disabledTracks:normalizeDisabledTracks(p)}}function listenSSE(){runtime.serverEvents?.close();const e=new EventSource(projectApi("/api/events"));runtime.serverEvents=e,e.onmessage=e=>{"projects"===e.data?(loadProjectCatalog().catch(()=>{}),syncFromServer()):syncFromServer()},e.onerror=()=>{runtime.connecting||syncFromServer()}}async function syncFromServer(e){if(runtime.connecting)return;if(state.gesture||state.exporting||$("coverDialog").open)return void(runtime.pendingSync=!0);runtime.pendingSync=!1,"project"!==state.binTab&&fetchLibrary(state.binTab).then(renderLibrary),loadLibraryFonts();const t=runtime.projectReadVersion=(runtime.projectReadVersion||0)+1;try{const n=await fetchProjectDocument();if(t!==runtime.projectReadVersion)return;if($("coverDialog").open)return void(runtime.pendingSync=!0);if(!e&&!runtime.projectLoadError&&(n.revision||0)===(project.revision||0))return;runtime.saveTimer&&(clearTimeout(runtime.saveTimer),runtime.saveTimer=null,toast(uiText("Project was updated externally — your last change may need redoing."))),applyProject(n),clearProjectLoadError(),await probeMissingMeta()}catch(e){t===runtime.projectReadVersion&&showProjectLoadError(e)}}async function loadMediaMetadata(e){const t=e.src,n=mediaSourceVersions.get(t);try{if("svg"===e.kind)return void await loadSvgMedia({...e,src:mediaLoadSrc(e)});if("image"===e.kind){const t=await loadImage(mediaLoadSrc(e));return runtime.mediaAux.set(e.id,{...runtime.mediaAux.get(e.id)||{},img:t}),e.width=t.naturalWidth,void(e.height=t.naturalHeight)}Object.assign(e,await probeAV(mediaLoadSrc(e),e.kind))}catch(e){throw mediaSourceVersions.get(t)===n&&setMediaFailure(t,"unreadable"),e}}const mediaFailures=new Map,mediaSourceVersions=new Map;function mediaLoadSrc(e){if(!e?.src)return"";const t=mediaSourceVersions.get(e.src);if(!t)return e.src;const n=new URL(e.src,location.href);return n.origin===location.origin&&/^https?:$/.test(n.protocol)?(n.searchParams.set("_fcRetry",t),n.href):e.src}let mediaCheckPromise=null;function mediaFailure(e){return e?.src?mediaFailures.get(e.src):"missing"}function clipMediaFailure(e){return["video","audio","image","svg"].includes(e.kind)?mediaFailure(getMedia(e.mediaId)):null}function mediaFailureLabel(e){return uiText("missing"===e?"Media missing":"Media unreadable")}function setMediaFailure(e,t){e&&project.media.some(t=>t.src===e)&&("unreadable"===t&&"missing"===mediaFailures.get(e)||mediaFailures.get(e)!==t&&(t?mediaFailures.set(e,t):mediaFailures.delete(e),t&&(clipThumbQueue=clipThumbQueue.filter(t=>t.src!==e)),renderBin(),state.dirtyTimeline=!0))}function resetMediaSource(e){mediaSourceVersions.set(e,String(Date.now()));for(const t of project.media.filter(t=>t.src===e)){runtime.mediaAux.delete(t.id),runtime.audioBufs.delete(t.id),runtime.wavePeaks.delete(t.id);for(const e of project.clips.filter(e=>e.mediaId===t.id))releaseClipEl(e.id)}for(const[t,n]of clipThumbCache)n.src===e&&clipThumbCache.delete(t);clipThumbQueue=clipThumbQueue.filter(t=>t.src!==e),clipThumbIndex=null,resetPreviewWait()}async function retryMediaSource(e){const t=e.src;resetMediaSource(t),mediaFailures.delete(t);for(const e of project.media.filter(e=>e.src===t))try{await loadMediaMetadata({...e})}catch{setMediaFailure(t,"unreadable")}renderBin(),state.dirtyTimeline=!0}async function checkMediaAvailability(e=!1){if(mediaCheckPromise||state.rendering||state.exporting&&!backgroundExportActive)return mediaCheckPromise;mediaCheckPromise=(async()=>{const t=[...new Map(project.media.filter(e=>e.src).map(e=>[e.src,e])).values()];await Promise.all(Array.from({length:Math.min(4,t.length)},async()=>{for(;t.length;){const n=t.shift(),i=n.src;try{const t=new URL(i,location.href);if(t.origin!==location.origin||!/^https?:$/.test(t.protocol)){e&&mediaFailures.has(i)&&await retryMediaSource(n);continue}const r=await fetch(t,{method:"HEAD",cache:"no-store",signal:AbortSignal.timeout(5e3)});if(!project.media.includes(n)||n.src!==i)continue;404===r.status||410===r.status?setMediaFailure(i,"missing"):r.ok||405===r.status||501===r.status?(r.ok||405===r.status||501===r.status)&&("missing"===mediaFailures.get(i)||e&&mediaFailures.has(i))&&await retryMediaSource(n):setMediaFailure(i,"unreadable")}catch{project.media.includes(n)&&n.src===i&&setMediaFailure(i,"unreadable")}}}))})();try{await mediaCheckPromise}finally{mediaCheckPromise=null}}function updateMissingMediaNotice(){const e=$("missingMediaNotice"),t=previewFrameTime(runtime.previewWait?.t??state.time),n=project.clips.filter(e=>isTrackEnabled(e.track)&&activeAt(e,t)&&clipMediaFailure(e));if(e.hidden=!n.length||state.rendering||state.exporting&&!backgroundExportActive,e.hidden)return;const i=n.some(e=>"missing"===clipMediaFailure(e)),r=mediaFailureLabel(i?"missing":"unreadable"),a=[...new Set(n.map(e=>getMedia(e.mediaId)?.name||e.name||e.mediaId||e.id))].join("、"),o=JSON.stringify([r,a]);e.dataset.key!==o&&(e.dataset.key=o,$("missingMediaTitle").textContent=r,$("missingMediaNames").textContent=a,$("missingMediaNames").title=a,$("missingMediaHelp").textContent=uiText(i?"The source file was deleted or moved. Restore it to its original location, then retry.":"The source file could not be loaded. Check the file or connection, then retry."))}async function probeMissingMeta(){checkMediaAvailability();let e=!1;for(const t of project.media)if("svg"!==t.kind){if("image"!==t.kind&&(null==t.duration||isNaN(t.duration)))try{await loadMediaMetadata(t),e=!0}catch{}if("image"===t.kind&&!runtime.mediaAux.get(t.id)?.img)try{await loadMediaMetadata(t),e=!0}catch{}"video"!==t.kind||runtime.mediaAux.get(t.id)?.thumb||grabThumb(t).catch(()=>{}),ensureWave(t)}else if(!runtime.mediaAux.get(t.id)?.svgText)try{await loadMediaMetadata(t),e=!0}catch{}e&&(renderBin(),scheduleSave()),state.dirtyTimeline=!0}function probeAV(e,t){return new Promise((n,i)=>{const r=document.createElement("audio"===t?"audio":"video"),a=(e,t)=>{clearTimeout(o),r.onloadedmetadata=r.onerror=null,r.removeAttribute("src"),r.load(),e?i(e):n(t)},o=setTimeout(()=>a(new Error("media metadata timed out")),1e4);r.onloadedmetadata=()=>a(null,{duration:r.duration,width:r.videoWidth||void 0,height:r.videoHeight||void 0}),r.onerror=()=>a(new Error(r.error?.message||"media load failed")),r.preload="metadata",r.src=e})}function loadImage(e){return new Promise((t,n)=>{const i=new Image,r=e=>{clearTimeout(a),i.onload=i.onerror=null,e?n(e):t(i)},a=setTimeout(()=>r(new Error("image load timed out")),1e4);i.onload=()=>r(),i.onerror=()=>r(new Error("image load failed")),i.src=e})}async function grabThumb(e){const t=e.src,n=mediaSourceVersions.get(t),i=document.createElement("video");i.muted=!0,i.preload="auto",i.src=mediaLoadSrc(e);try{await new Promise((e,t)=>{i.onloadeddata=e,i.onerror=t}),i.currentTime=Math.min(.5,(i.duration||1)/2),await new Promise(e=>{i.onseeked=e,setTimeout(e,1500)}),runtime.mediaAux.set(e.id,{...runtime.mediaAux.get(e.id)||{},thumb:videoThumb(i)}),renderBin(),state.dirtyTimeline=!0}catch(e){throw i.error&&mediaSourceVersions.get(t)===n&&setMediaFailure(t,"unreadable"),e}finally{i.pause(),i.removeAttribute("src"),i.load()}}function videoThumb(e){const t=document.createElement("canvas"),n=Math.min(160/e.videoWidth,160/e.videoHeight,1);return t.width=Math.max(1,Math.round(e.videoWidth*n)),t.height=Math.max(1,Math.round(e.videoHeight*n)),t.getContext("2d").drawImage(e,0,0,t.width,t.height),t.toDataURL("image/jpeg",.6)}$("btnRetryMedia").addEventListener("click",async()=>{const e=$("btnRetryMedia");e.disabled=!0;try{await mediaCheckPromise,await checkMediaAvailability(!0)}finally{e.disabled=!1}}),window.addEventListener("focus",()=>{checkMediaAvailability()}),setInterval(()=>{previewUnavailable()||checkMediaAvailability()},1e4);const clipThumbCache=new Map;let clipThumbIndex=null,clipThumbQueue=[],clipThumbActive=0,visibleThumbKeys=new Set,neededThumbKeys=new Set,clipThumbsChanged=!1,clipThumbProgressDirty=!0;const clipThumbVideos=[],clipThumbDecodes=new Set;let clipThumbDecodeWaiters=0;function releaseClipThumbVideo(e){e.video.pause(),e.video.removeAttribute("src"),e.video.load()}function acquireClipThumbVideo(e){const t=clipThumbVideos.findIndex(t=>t.src===e),n=clipThumbVideos.splice(t<0?0:t,1)[0]||{video:document.createElement("video"),src:null};return n.src!==e&&(n.video.muted=!0,n.video.preload="auto",n.video.src=e,n.video.load(),n.src=e),n}const clipThumbDiskSources=new Map;let clipThumbDiskCache,clipThumbDiskDisabled=!1,clipThumbDiskWrites=0,clipThumbDiskWrite=Promise.resolve();const clipThumbDiskQueue=new Map;let clipThumbDiskWriting=!1;async function withClipThumbStorage(e){if(clipThumbDiskDisabled||"undefined"==typeof caches)return null;let t;try{return await Promise.race([e(),new Promise(e=>{t=setTimeout(()=>{clipThumbDiskDisabled=!0,e(null)},2e3)})])}catch{return clipThumbDiskDisabled=!0,null}finally{clearTimeout(t)}}function validStoredClipThumb(e){return"string"==typeof e&&e.length<=65536&&/^data:image\/jpeg;base64,[A-Za-z0-9+/]+=*$/.test(e)}async function clipThumbSourceVersion(e){const t=new URL(e,location.href);if(t.origin!==location.origin||!/^https?:$/.test(t.protocol))return null;const n=mediaLoadSrc({src:e});return clipThumbDiskSources.has(n)||clipThumbDiskSources.set(n,(async()=>{try{const e=await fetch(n,{method:"HEAD",cache:"no-store",signal:AbortSignal.timeout(1500)}),t=e.headers.get("ETag");return e.ok&&t?t:null}catch{return null}})()),clipThumbDiskSources.get(n)}async function readStoredClipThumb(e){return withClipThumbStorage(async()=>{const t=new URL(e.src,location.href),n=await clipThumbSourceVersion(e.src);if(!n||clipThumbDiskDisabled)return null;const i=await(clipThumbDiskCache||=caches.open("fablecut-timeline-thumbs-v1")),r=location.origin+"/__fablecut_thumbnails__/"+encodeURIComponent(JSON.stringify([t.href,n,e.time]));e.disk={cache:i,key:r};const a=await i.match(r),o=a?await a.text():null;if(validStoredClipThumb(o)){const e=new Image;e.src=o;try{if(await e.decode(),e.naturalWidth&&e.naturalHeight)return o}catch{}}return a&&await i.delete(r),null})}function storeClipThumb(e){if(clipThumbDiskDisabled||!e.disk||!validStoredClipThumb(e.thumb))return;const{cache:t,key:n}=e.disk;return clipThumbDiskQueue.delete(n),clipThumbDiskQueue.set(n,{cache:t,key:n,thumb:e.thumb}),clipThumbDiskQueue.size>1024&&clipThumbDiskQueue.delete(clipThumbDiskQueue.keys().next().value),clipThumbDiskWriting||(clipThumbDiskWriting=!0,clipThumbDiskWrite=(async()=>{try{for(;clipThumbDiskQueue.size&&!clipThumbDiskDisabled;){const e=clipThumbDiskQueue.values().next().value;clipThumbDiskQueue.delete(e.key),await withClipThumbStorage(async()=>{if(await e.cache.put(e.key,new Response(e.thumb)),clipThumbDiskWrites++%32==0){const t=await e.cache.keys();for(const n of t.slice(0,Math.max(0,t.length-992)))await e.cache.delete(n)}})}}finally{clipThumbDiskQueue.clear(),clipThumbDiskWriting=!1}})()),clipThumbDiskWrite}function clipThumbKey(e,t=0){const n=getMedia(e.mediaId);if("video"!==e.kind||!n?.src||mediaFailures.has(n.src))return null;const i=mediaTimeAt(e,e.start+clamp(t,0,Math.max(0,e.duration-.001)));return JSON.stringify([n.src,+Math.max(0,i||0).toFixed(3)])}function nearbyClipThumb(e,t){const n=clipThumbKey(e,t);if(!n)return null;const[i,r]=JSON.parse(n);if(!clipThumbIndex){clipThumbIndex=new Map;for(const e of clipThumbCache.values())e.thumb&&(clipThumbIndex.has(e.src)||clipThumbIndex.set(e.src,[]),clipThumbIndex.get(e.src).push(e));for(const e of clipThumbIndex.values())e.sort((e,t)=>e.time-t.time)}const a=clipThumbIndex.get(i);if(!a?.length)return null;const o=JSON.parse(clipThumbKey(e,0))[1],s=JSON.parse(clipThumbKey(e,e.duration))[1];let l=0,c=a.length;for(;l<c;){const e=l+c>>>1;a[e].time<r?l=e+1:c=e}let d=null;for(const e of[a[l-1],a[l]])!e||e.time<o||e.time>s||clipThumbCache.get(e.key)!==e||(!d||Math.abs(e.time-r)<Math.abs(d.time-r))&&(d=e);return d?(neededThumbKeys.add(d.key),d.thumb):null}function pruneClipThumbs(){clipThumbProgressDirty=!0;const e=new Set(project.media.map(e=>e.src));for(const[t,n]of clipThumbCache)(!e.has(n.src)||clipThumbCache.size>512&&!neededThumbKeys.has(t))&&clipThumbCache.delete(t);for(const e of clipThumbQueue)neededThumbKeys.has(e.key)||clipThumbCache.delete(e.key);clipThumbQueue=clipThumbQueue.filter(e=>clipThumbCache.get(e.key)===e),clipThumbQueue.sort((e,t)=>Number(visibleThumbKeys.has(t.key))-Number(visibleThumbKeys.has(e.key))),clipThumbIndex=null}function clipThumb(e,t=0,n=!0){const i=clipThumbKey(e,t);if(!i)return null;neededThumbKeys.add(i),n&&visibleThumbKeys.add(i);let r=clipThumbCache.get(i);if(r)clipThumbCache.delete(i),clipThumbCache.set(i,r);else{const[t,a]=JSON.parse(i);r={key:i,src:t,time:a,clip:{id:e.id,mediaId:e.mediaId},thumb:null},clipThumbCache.set(i,r),clipThumbQueue.push(r),clipThumbProgressDirty=!0,n&&loadClipThumbs()}return r.thumb}function clipThumbWidth(e,t){const n=getMedia(e.mediaId),i=Math.max(8,t-8);return Math.max(16,i*((n?.width||16)/(n?.height||9)))}function preloadClipThumbs(){if(trackSizeShowsThumbs())for(const e of project.clips){const t=trackOf(e);if("video"!==e.kind||!t)continue;const n=clipThumbWidth(e,t.h),i=Math.ceil(e.duration*state.pps/n);for(let t=0;t<i;t++){const i=t*n/state.pps;clipThumb(e,i,!1)||nearbyClipThumb(e,i)}}}function clipThumbStrip(e,t,n=state.pps,i=els.timelineScroll){const r=clipThumbWidth(e,t),a=e.duration*n,o=e.start*n,s=Math.max(0,Math.floor((i.scrollLeft-o)/r)),l=Math.min(Math.ceil(a/r),Math.ceil((i.scrollLeft+i.clientWidth-o)/r));let c="";for(let t=s;t<l;t++){const i=t*r/n,a=clipThumb(e,i)||nearbyClipThumb(e,i);c+=`<div class="thumbs" data-thumb-key="${encodeURIComponent(clipThumbKey(e,i))}" style="left:${t*r}px;width:${r+1}px;${a?`background-image:url('${a}')`:""}"></div>`}return c?`<div class="clip-thumb-strip">${c}</div>`:""}function loadClipThumbs(){if(state.exporting||state.rendering||previewUnavailable()||!clipThumbQueue.length)clipThumbVideos.splice(0).forEach(releaseClipThumbVideo);else for(;clipThumbActive<2&&clipThumbQueue.length&&!clipThumbDecodeWaiters;){const e=clipThumbQueue.shift();clipThumbActive++,(async()=>{let t,n,i,r=!1;try{const a="undefined"!=typeof caches?await readStoredClipThumb(e):null;if(clipThumbCache.get(e.key)!==e)return;if(!neededThumbKeys.has(e.key))return void clipThumbCache.delete(e.key);if(a)return e.thumb=a,void(clipThumbIndex=null);if(state.playing&&clipThumbDecodes.size){clipThumbDecodeWaiters++;try{for(;state.playing&&clipThumbDecodes.size;)await Promise.race(clipThumbDecodes)}finally{clipThumbDecodeWaiters--}}if(clipThumbCache.get(e.key)!==e)return;if(!neededThumbKeys.has(e.key))return void clipThumbCache.delete(e.key);if(state.exporting||state.rendering||previewUnavailable())return void clipThumbQueue.unshift(e);n=new Promise(e=>{i=e}),clipThumbDecodes.add(n),t=acquireClipThumbVideo(mediaLoadSrc({src:e.src}));const o=t.video;await prepareVideoFrame(e.clip,o,e.time),r=!0,clipThumbCache.get(e.key)===e&&(e.thumb=videoThumb(o),clipThumbIndex=null,storeClipThumb(e))}catch{e.failed=!0}finally{n&&(clipThumbDecodes.delete(n),i()),t&&(r&&clipThumbQueue.length?clipThumbVideos.push(t):releaseClipThumbVideo(t)),clipThumbActive--,clipThumbProgressDirty=!0,clipThumbCache.get(e.key)===e&&e.thumb&&(clipThumbsChanged=!0),loadClipThumbs()}})()}}function clipThumbProgress(){let e=0,t=0,n=0;for(const i of neededThumbKeys){const r=clipThumbCache.get(i);r&&!mediaFailures.has(r.src)&&(e++,r.thumb?t++:r.failed&&n++)}return{total:e,loaded:t,failed:n,pending:e-t-n}}function renderClipThumbProgress(){if(!clipThumbProgressDirty||!els.thumbProgress)return;clipThumbProgressDirty=!1;const e=els.timelineScroll.clientHeight,{total:t,loaded:n,failed:i,pending:r}=clipThumbProgress();els.thumbProgress.hidden=!r&&!i,els.thumbProgressBar.hidden=!r,els.thumbProgressBar.max=Math.max(1,t),els.thumbProgressBar.value=n,els.thumbProgressLabel.textContent=uiText(r?"Thumbnails {loaded}/{total}":"{failed} thumbnails unavailable",{loaded:n,total:t,failed:i}),els.timelineScroll.clientHeight!==e&&(state.dirtyTimeline=!0)}function updateClipThumbs(){if(clipThumbsChanged){clipThumbsChanged=!1;for(const e of[...els.tracksContent.querySelectorAll(".thumbs[data-thumb-key]"),...els.coverFrames?.querySelectorAll(".thumbs[data-thumb-key]")||[]]){const t=clipThumbCache.get(decodeURIComponent(e.dataset.thumbKey))?.thumb;if(t){const n=`url(${JSON.stringify(t)})`;e.style.backgroundImage!==n&&(e.style.backgroundImage=n)}}}renderClipThumbProgress(),loadClipThumbs()}let decodeCtx=null;function getDecodeCtx(){return decodeCtx||(decodeCtx=new(window.AudioContext||window.webkitAudioContext))}function getAudioBuffer(e){let t=runtime.audioBufs.get(e.id);return t||(t=fetch(mediaLoadSrc(e)).then(e=>e.arrayBuffer()).then(e=>getDecodeCtx().decodeAudioData(e)),runtime.audioBufs.set(e.id,t),t.catch(()=>runtime.audioBufs.delete(e.id))),t}const BROWSER_DECODE_MAX_SECONDS=600;function browserDecodable(e){return Number.isFinite(e.duration)&&e.duration<=600}async function serverWave(e){const t=await fetch(projectApi("/api/waveform?src="+encodeURIComponent(e.src)),{cache:"no-store"}),n=await t.json();if(!t.ok||50!==n.peaksPerSecond)throw new Error(n.error||"waveform unavailable");return{channels:n.peaks.map(e=>{const t=Uint8Array.from(atob(e),e=>e.charCodeAt(0));return Float32Array.from(t,e=>e/255)}),numberOfChannels:n.channels}}async function browserWave(e){const t=await getAudioBuffer(e),n=Math.max(1,Math.ceil(50*t.duration)),i=t.length/n,r=[];for(let e=0;e<t.numberOfChannels;e++){const a=t.getChannelData(e),o=new Float32Array(n);for(let e=0;e<n;e++){let t=0;const n=Math.floor(e*i),r=Math.min(a.length,Math.floor((e+1)*i));for(let e=n;e<r;e+=8){const n=Math.abs(a[e]);n>t&&(t=n)}o[e]=t}r.push(o)}return{channels:r,numberOfChannels:t.numberOfChannels}}function ensureWave(e){"audio"!==e.kind&&"video"!==e.kind||runtime.wavePeaks.has(e.id)||project.clips.some(t=>"audio"===t.kind&&t.mediaId===e.id)&&(runtime.wavePeaks.set(e.id,null),serverWave(e).catch(t=>{if(!browserDecodable(e))throw t;return browserWave(e)}).then(({channels:t,numberOfChannels:n})=>{const i=Math.max(1,...t.map(e=>e.length)),r=new Float32Array(i);for(let e=0;e<i;e++){let n=0;for(const i of t)i[e]>n&&(n=i[e]);r[e]=n}runtime.wavePeaks.set(e.id,{channels:t,max:r}),null==e.channels&&(e.channels=n),state.dirtyTimeline=!0}).catch(()=>runtime.wavePeaks.delete(e.id)))}function wavePeaksFor(e){const t=runtime.wavePeaks.get(e.mediaId);if(!t)return null;if(t instanceof Float32Array)return t;if(!t.max)return null;const n=e.props?.audioChannel;return Number.isInteger(n)&&t.channels?.[n]?t.channels[n]:t.max}function mediaKindFromFile(e){const t=e.type||"";if("image/svg+xml"===t||"image/svg"===t)return"svg";if(t.startsWith("video/"))return"video";if(t.startsWith("audio/"))return"audio";if(t.startsWith("image/"))return"image";const n=(e.name.split(".").pop()||"").toLowerCase();return"svg"===n?"svg":["mp4","mov","webm","mkv","m4v","avi","mpg","mpeg"].includes(n)?"video":["mp3","wav","m4a","aac","ogg","flac","wma"].includes(n)?"audio":["png","jpg","jpeg","gif","webp","bmp","avif"].includes(n)?"image":null}async function importFiles(e){if(runtime.projectLoadError)return[];const t=runtime.projectEpoch,n=[...e];if(!n.length)return[];const i=runtime.importFolderId&&getFolder(runtime.importFolderId)?runtime.importFolderId:null;runtime.importFolderId=null;let r=0,a=0;const o=[];for(const e of n){if(runtime.projectLoadError||runtime.projectEpoch!==t)break;const n=mediaKindFromFile(e);if(!n){a++;continue}let s,l=!1;if(state.connected)try{const t=await fetch(projectApi("/api/upload?name="+encodeURIComponent(e.name)),{method:"POST",body:e});if(!t.ok)throw new Error("upload "+t.status);s=(await t.json()).src}catch{s=URL.createObjectURL(e),l=!0}else s=URL.createObjectURL(e),l=!0;const c={id:"m_"+uid(),name:e.name,kind:n,src:s,transient:l,folderId:i};try{await loadMediaMetadata(c),"video"===n&&grabThumb(c).catch(()=>{}),ensureWave(c)}catch{a++;continue}if(runtime.projectLoadError||runtime.projectEpoch!==t){l&&URL.revokeObjectURL(s);break}project.media.push(c),o.push(c),r++}return renderBin(),scheduleSave(),!r&&a?toast(uiText("Couldn't import — unsupported or unreadable file type")):a&&toast(uiText("Imported {added}, skipped {skipped}",{added:r,skipped:a})),o}function renderBin(){els.binList.querySelectorAll(".bin-item, .bin-folder, .bin-drop-root").forEach(e=>e.remove());const e=!project.media.length&&!project.folders.length;if(els.binEmpty.style.display=e?"":"none",e)return;const t=()=>{els.binList.querySelectorAll(".bin-drop-over").forEach(e=>e.classList.remove("bin-drop-over"))},n=(e,n)=>{e.addEventListener("dragover",i=>{const r=[...i.dataTransfer?.types||[]],a=r.includes("text/fablecut-media"),o=r.includes("text/fablecut-folder"),s=r.includes("Files");if(a||o||s){if(o){const e=runtime.binDragFolderId;if(e&&n&&isSelfOrFolderAncestor(e,n))return}i.preventDefault(),i.stopPropagation(),i.dataTransfer.dropEffect=s?"copy":"move",t(),e.classList.add("bin-drop-over"),s&&(runtime.importFolderId=n)}}),e.addEventListener("dragleave",t=>{e.contains(t.relatedTarget)||(e.classList.remove("bin-drop-over"),runtime.importFolderId===n&&(runtime.importFolderId=null))}),e.addEventListener("drop",e=>{const i=e.dataTransfer.getData("text/fablecut-media"),r=e.dataTransfer.getData("text/fablecut-folder"),a=e.dataTransfer.files;return t(),a?.length?(e.preventDefault(),e.stopPropagation(),runtime.importFolderId=n,void importFiles(a)):i?(e.preventDefault(),e.stopPropagation(),void moveMediaToFolder(i,n)):void(r&&(e.preventDefault(),e.stopPropagation(),moveFolderToParent(r,n)))})},i=(e,n)=>{const i=document.createElement("div"),r=mediaFailure(e);i.className="bin-item"+(r?" media-missing":""),i.draggable=!0,i.dataset.mediaId=e.id,i.style.setProperty("--bin-depth",n);const a=runtime.mediaAux.get(e.id)||{},o=mediaKindIcon(e.kind,"🎞"),s=a.thumb||("image"===e.kind||"svg"===e.kind?e.src:null);i.innerHTML=`\n <div class="bin-thumb${"video"===e.kind?" bin-thumb-video":""}"></div>\n <div class="bin-meta">\n <div class="bin-name"></div>\n <div class="bin-sub">${uiText(e.kind)}${e.duration?" · "+fmt(e.duration):""}</div>\n </div>\n <span class="bin-del" title="${uiAttr("Remove (and its clips)")}">✕</span>`;const l=i.querySelector(".bin-thumb");r?(l.textContent="⚠",i.querySelector(".bin-sub").textContent=mediaFailureLabel(r)):s?l.style.backgroundImage=`url(${JSON.stringify(s)})`:l.textContent=o;const c=i.querySelector(".bin-name");return c.textContent=e.name,c.title=e.name,i.addEventListener("dragstart",t=>{runtime.binDragFolderId=null,runtime.dragMediaId=e.id,t.dataTransfer.setData("text/fablecut-media",e.id),t.dataTransfer.effectAllowed="copyMove",i.classList.add("bin-dragging")}),i.addEventListener("dragend",()=>{i.classList.remove("bin-dragging"),t(),runtime.importFolderId=null}),i.addEventListener("dragover",e=>e.stopPropagation()),i.addEventListener("click",t=>{t.target.closest(".bin-del")||t.ctrlKey||t.metaKey||getSetting("linkSelect")&&(t.preventDefault(),selectClipsByMediaId(e.id))}),i.addEventListener("dblclick",()=>addClipFromMedia(e,null,state.time)),i.querySelector(".bin-del").addEventListener("click",t=>{t.stopPropagation(),pushUndo(),project.media=project.media.filter(t=>t.id!==e.id),project.clips=project.clips.filter(t=>t.mediaId!==e.id),renderBin(),scheduleSave(),renderInspector()}),i},r=(e,a)=>{for(const i of folderChildren(e)){const e=document.createElement("div");e.className="bin-folder"+(i.open?" open":""),e.dataset.folderId=i.id,e.draggable=!0,e.style.setProperty("--bin-depth",a);const o=mediaInFolder(i.id).length+folderChildren(i.id).length;e.innerHTML=`\n <button type="button" class="bin-folder-twist" title="${i.open?uiText("Collapse"):uiText("Expand")}" aria-expanded="${i.open}">${i.open?"▼":"▶"}</button>\n <span class="bin-folder-icon">📁</span>\n <span class="bin-folder-name"></span>\n <span class="bin-folder-count">${o}</span>\n <button type="button" class="bin-folder-add" title="${uiAttr("New subfolder")}">+</button>\n <button type="button" class="bin-del bin-folder-del" title="${uiAttr("Delete folder (keeps media)")}">✕</button>`;const s=e.querySelector(".bin-folder-name");s.textContent=i.name,s.title=i.name,e.querySelector(".bin-folder-twist").addEventListener("click",e=>{e.stopPropagation(),i.open=!i.open,scheduleSave(),renderBin()}),e.querySelector(".bin-folder-name").addEventListener("dblclick",e=>{e.stopPropagation(),startFolderRename(i.id)}),e.querySelector(".bin-folder-add").addEventListener("click",e=>{e.stopPropagation(),addFolder(i.id)}),e.querySelector(".bin-folder-del").addEventListener("click",e=>{e.stopPropagation(),deleteFolder(i.id)}),e.addEventListener("dragstart",t=>{t.target.closest("button")?t.preventDefault():(runtime.binDragFolderId=i.id,t.dataTransfer.setData("text/fablecut-folder",i.id),t.dataTransfer.effectAllowed="move",e.classList.add("bin-dragging"))}),e.addEventListener("dragend",()=>{runtime.binDragFolderId=null,e.classList.remove("bin-dragging"),t(),runtime.importFolderId=null}),n(e,i.id),els.binList.appendChild(e),i.open&&r(i.id,a+1)}for(const t of mediaInFolder(e))els.binList.appendChild(i(t,a))};r(null,0);const a=document.createElement("div");a.className="bin-drop-root",a.textContent=uiText("Drop here to move to root"),n(a,null),els.binList.appendChild(a),syncBinSelectionFromTimeline()}function openFileImport(e,t){const n=els.fileInput;null!=e&&null!=t&&(n.style.cssText=`position:fixed;left:${e}px;top:${t}px;width:1px;height:1px;opacity:0;margin:0;padding:0;border:0;overflow:hidden;z-index:-1;`),setTimeout(()=>n.click(),0)}async function fetchLibrary(e){try{runtime.library[e]=await(await fetch("/api/library?dir="+e)).json()}catch{runtime.library[e]=[]}return runtime.library[e]}function libKind(e){return/\.svg$/i.test(e)?"svg":AUDIO_EXT.test(e)?"audio":VIDEO_EXT.test(e)?"video":IMAGE_EXT.test(e)?"image":null}function mediaKindIcon(e,t){return"audio"===e?"🎵":"svg"===e?"✨":"image"===e?"🖼":"video"===e?"🎞":t}function mediaForLibraryItem(e){let t=project.media.find(t=>t.src===e.src);if(t)return t;const n=libKind(e.name);return n?(t={id:"m_"+uid(),name:e.name,kind:n,src:e.src,folderId:null},project.media.push(t),renderBin(),scheduleSave(),t):null}async function addLibraryItem(e,t,n,i){const r=mediaForLibraryItem(e);if(r){if(("audio"===r.kind||"video"===r.kind)&&(null==r.duration||isNaN(r.duration))){try{await loadMediaMetadata(r)}catch{}ensureWave(r),"video"===r.kind&&grabThumb(r).catch(()=>{})}if("svg"===r.kind&&!runtime.mediaAux.get(r.id)?.svgText)try{await loadMediaMetadata(r)}catch{}if("image"===r.kind&&!runtime.mediaAux.get(r.id)?.img)try{await loadMediaMetadata(r)}catch{}addClipFromMedia(r,t,n,i)}else toast(uiText("Unsupported file type: ")+e.name)}function toggleSfxPreview(e,t){if(previewUnavailable())return;const n=runtime.sfxPreview;if(n&&n.dataset.src===e.src&&!n.paused)return n.pause(),void(t.textContent="▶");n&&n.pause(),els.libList.querySelectorAll(".lib-play").forEach(e=>e.textContent="▶");const i=new Audio(e.src);i.dataset.src=e.src,i.onended=()=>{t.textContent="▶"},i.play().catch(()=>toast(uiText("Couldn't play ")+e.name)),t.textContent="⏸",runtime.sfxPreview=i}function renderLibrary(){const e=state.binTab;if("project"===e)return;const t=runtime.library[e]||[];if(els.libList.innerHTML="",t.length)for(const n of t){const t=libKind(n.name),i=document.createElement("div");i.className="bin-item lib-item",i.draggable=!0;const r="image"===t||"svg"===t,a=mediaKindIcon(t,"🧩");i.innerHTML=`\n <div class="bin-thumb${"svg"===t?" svg":""}"></div>\n <div class="bin-meta">\n <div class="bin-name"></div>\n <div class="bin-sub">${(n.size/1024).toFixed(0)} KB</div>\n </div>\n ${"sfx"===e?`<button class="btn tiny lib-play" title="${uiAttr("Preview")}">▶</button>`:""}\n <button class="btn tiny accent lib-add" title="${uiAttr("Add at playhead")}">+</button>`;const o=i.querySelector(".bin-thumb");r?o.style.backgroundImage=`url(${JSON.stringify(n.src)})`:o.textContent=a;const s=i.querySelector(".bin-name");s.textContent=n.name,s.title=n.rel||n.name,i.addEventListener("dragstart",e=>{const t=mediaForLibraryItem(n);t?(runtime.dragMediaId=t.id,e.dataTransfer.setData("text/fablecut-media",t.id),e.dataTransfer.effectAllowed="copy"):e.preventDefault()}),i.addEventListener("dblclick",()=>addLibraryItem(n,null,state.time)),i.querySelector(".lib-add").addEventListener("click",()=>addLibraryItem(n,null,state.time));const l=i.querySelector(".lib-play");l&&l.addEventListener("click",()=>toggleSfxPreview(n,l)),els.libList.appendChild(i)}else els.libList.innerHTML=`<div class="bin-empty">\n <div class="bin-empty-icon">${"sfx"===e?"🔊":"svg"===e?"✨":"🧩"}</div>\n <p>${uiText("No assets yet.")}</p>\n <p class="hint">${uiText("Drop files into")}<br><b>library/${e}/</b><br>${uiText("— this list live-updates.")}</p></div>`}function setBinTab(e){state.binTab=e;for(const t of els.binTabs.querySelectorAll("[data-tab]"))t.classList.toggle("on",t.dataset.tab===e);const t="project"===e;els.binList.classList.toggle("hidden",!t),els.libList.classList.toggle("hidden",t),t||fetchLibrary(e).then(renderLibrary)}function syncHistoryButtons(){$("btnUndo").disabled=0===runtime.undo.length,$("btnRedo").disabled=0===runtime.redo.length}function pushUndo(){runtime.undo.push(timelineSnapshot()),runtime.undo.length>100&&runtime.undo.shift(),runtime.redo.length=0,syncHistoryButtons()}function undo(){runtime.undo.length&&(runtime.redo.push(timelineSnapshot()),restoreTimeline(runtime.undo.pop()),pruneSelection(),scheduleSave(),renderInspector())}function redo(){runtime.redo.length&&(runtime.undo.push(timelineSnapshot()),restoreTimeline(runtime.redo.pop()),pruneSelection(),scheduleSave(),renderInspector())}function defaultTrackFor(e){return"audio"===e?"A1":"svg"===e?"V3":"V1"}function linkedClip(e){return e?.linkedId?getClip(e.linkedId):null}function withLinked(e){const t=new Map,n=new Set;for(const i of e)if(t.set(i.id,i),i.linkGroup)n.add(i.linkGroup);else{const e=linkedClip(i);e&&t.set(e.id,e)}if(n.size)for(const e of project.clips)e.linkGroup&&n.has(e.linkGroup)&&t.set(e.id,e);return[...t.values()]}function syncLinkedTiming(e){for(const t of withLinked([e]))t.id!==e.id&&(t.start=e.start,t.in=e.in,t.duration=e.duration,null!=e.props?.speed&&(t.props=t.props||{},t.props.speed=e.props.speed))}function syncLinkedSpeed(e){for(const t of withLinked([e]))t.id!==e.id&&(t.props=t.props||{},t.props.speed=clipSpeed(e),e.keyframes?.speed?.length?(t.keyframes=t.keyframes||{},t.keyframes.speed=e.keyframes.speed.map(e=>({...e}))):t.keyframes?.speed&&(delete t.keyframes.speed,Object.keys(t.keyframes).length||(t.keyframes=void 0)));state.dirtyTimeline=!0}function relinkClips(){const e=(e,t)=>Math.abs((+e||0)-(+t||0))<.001;for(const e of project.clips){if(e.linkedId&&!e.linkGroup){const t=getClip(e.linkedId);t&&(e.linkGroup=t.linkGroup=t.linkGroup||"lg_"+uid())}delete e.linkedId}const t=project.clips.filter(e=>"audio"===e.kind&&e.mediaId),n=new Set;for(const i of project.clips){if("video"!==i.kind||!i.mediaId||i.linkGroup)continue;const r=t.filter(t=>!n.has(t.id)&&!t.linkGroup&&t.mediaId===i.mediaId&&e(t.start,i.start)&&e(t.in,i.in)&&e(t.duration,i.duration));if(!r.length)continue;const a="lg_"+uid();i.linkGroup=a;for(const e of r)e.linkGroup=a,n.add(e.id)}}function addClipFromMedia(e,t,n,i){pushUndo();const r=e.kind;i?.newTrack&&(t=Timeline.insertTrack(project,Timeline.kindOf(e),i.beforeId)),ensureTrack(t=t||defaultTrackFor(r));const a=TRACKS.find(e=>e.id===t);a&&"audio"===r==("audio"===a.kind)||(t=defaultTrackFor(r));const o=e.duration||5,s=i?Timeline.firstGap(project.clips,t,n??state.time,o):Math.max(0,n??state.time),l=e.name.replace(/\.[^.]+$/,""),c={id:"c_"+uid(),mediaId:e.id,kind:r,track:t,start:s,in:0,duration:o,name:l,props:{...DEFAULT_PROPS}};if(project.clips.push(c),"video"===r){c.props.volume=0,ensureAudioTrackCount(2);const t="lg_"+uid();c.linkGroup=t;const n={id:"c_"+uid(),mediaId:e.id,kind:"audio",track:"A1",start:s,in:0,duration:o,name:l,props:{...DEFAULT_PROPS,audioChannel:0},linkGroup:t},i={id:"c_"+uid(),mediaId:e.id,kind:"audio",track:"A2",start:s,in:0,duration:o,name:l,props:{...DEFAULT_PROPS,audioChannel:1},linkGroup:t};project.clips.push(n,i),reconcileAudioChannels(c)}return ensureWave(e),selectClip(c.id),scheduleSave(),c}async function detectChannelCount(e){if(null!=e.channels)return e.channels;try{const t=await fetch(projectApi("/api/audio-info?src="+encodeURIComponent(e.src)),{cache:"no-store"}),n=await t.json();if(t.ok&&Number.isInteger(n.channels))return null==e.channels&&(e.channels=n.channels),e.channels}catch{}try{if(!browserDecodable(e))throw new Error("source too long to decode in the browser");const t=await getAudioBuffer(e);null==e.channels&&(e.channels=t.numberOfChannels)}catch{null==e.channels&&(e.channels=2)}return e.channels}async function reconcileAudioChannels(e){if("video"!==e.kind||!e.linkGroup)return;const t=e.mediaId,n=getMedia(t);if(!n)return;const i=await detectChannelCount(n);state.gesture&&await new Promise(e=>window.addEventListener("timelinegestureend",e,{once:!0}));const r=getClip(e.id);if(!r||r.mediaId!==t)return;const a=r.linkGroup,o=AUDIO_TRACK_IDS.length;i>o&&ensureAudioTrackCount(i);const s=AUDIO_TRACK_IDS.length-o,l=Math.min(i,16),c=project.clips.filter(e=>e.linkGroup===a&&"audio"===e.kind);let d=0,u=0;for(const e of c)(e.props?.audioChannel??0)>=l&&(releaseClipEl(e.id),project.clips=project.clips.filter(t=>t!==e),u++);for(let e=0;e<l;e++)c.some(t=>t.props?.audioChannel===e)||(project.clips.push({id:"c_"+uid(),mediaId:t,kind:"audio",track:"A"+(e+1),start:r.start,in:r.in,duration:r.duration,name:r.name,props:{...DEFAULT_PROPS,audioChannel:e},linkGroup:a}),d++);(d||u)&&(state.dirtyTimeline=!0,scheduleSave(),renderInspector(),i>16?toast(uiText("{name}: {count} audio channels, only {max} channels supported — extra channels dropped",{name:n.name,count:i,max:16})):d?toast(s?uiText("{name}: added {tracks} audio tracks and linked {channels} channels",{name:n.name,tracks:s,channels:l}):uiText("Linked {count} audio channels from {name}",{count:l,name:n.name})):u&&toast(uiText("{name} has fewer channels — removed {count} linked audio clips",{name:n.name,count:u})))}function applyTitleStyle(e,t,{keepTransform:n=!1}={}){const i=TITLE_STYLES[t]||TITLE_STYLES.plain,r=e.props,a=n?{x:r.x,y:r.y,scale:r.scale,rotation:r.rotation,align:r.align}:null;if(Object.assign(r,STYLE_RESET,i.props),a)Object.assign(r,a);else{const e=project.height||720,t=project.width||1280,n=i.place||"center";r.x=0,r.y="lower"===n?Math.round(.3*e):"upper"===n?-Math.round(.3*e):"lower-left"===n?Math.round(.28*e):0,"lower-left"===n&&(r.x=-Math.round(.18*t),r.align="left")}e.styleName=t}function openStylePicker(e,t){const n={props:JSON.parse(JSON.stringify(t.props)),styleName:t.styleName};let i=!1;const r=()=>{i||(t.props=JSON.parse(JSON.stringify(n.props)),t.styleName=n.styleName)},a=document.createElement("div");a.className="style-menu";for(const[e,n]of Object.entries(TITLE_STYLES)){const o=document.createElement("div");o.className="style-opt"+(t.styleName===e?" on":""),o.textContent=uiText(n.label),o.style.fontFamily=`"${n.props.font}", sans-serif`,n.props.uppercase&&(o.style.textTransform="uppercase"),o.addEventListener("mouseenter",()=>{r(),applyTitleStyle(t,e,{keepTransform:!0})}),o.addEventListener("click",()=>{r(),pushUndo(),applyTitleStyle(t,e,{keepTransform:!0}),i=!0,s(),scheduleSave(),renderInspector()}),a.appendChild(o)}a.addEventListener("mouseleave",r);const o=t=>{a.contains(t.target)||t.target===e||s()};function s(){r(),a.remove(),document.removeEventListener("pointerdown",o,!0),runtime.styleMenu=null}document.addEventListener("pointerdown",o,!0);const l=e.getBoundingClientRect();a.style.left=Math.round(Math.min(l.left,innerWidth-220))+"px",a.style.top=Math.round(Math.min(l.bottom+4,innerHeight-340))+"px",document.body.appendChild(a),runtime.styleMenu={close:s}}function openFontPicker(e,t){closeFontPicker();const n=document.createElement("div");n.className="style-menu font-menu",n.id="font-picker-menu",n.tabIndex=-1,n.setAttribute("role","listbox"),n.setAttribute("aria-label",uiText("Family"));const i=[],r=new Set,a=(e,a)=>{const o=a.filter(e=>!r.has(e)&&r.add(e));if(!o.length)return;const s=document.createElement("div");s.setAttribute("role","group"),s.setAttribute("aria-label",uiText(e));const c=document.createElement("div");c.className="font-group-label",c.textContent=uiText(e),c.setAttribute("aria-hidden","true"),s.appendChild(c);for(const e of o){const n=document.createElement("div");n.className="style-opt font-opt"+(e===t.props.font?" on":""),n.id=`font-picker-option-${i.length}`,n.setAttribute("role","option"),n.setAttribute("aria-selected",String(e===t.props.font)),n.dataset.font=e,n.textContent=n.title=e,n.style.fontFamily=`${JSON.stringify(e)}, sans-serif`,n.addEventListener("click",()=>l(e)),i.push(n),s.appendChild(n)}n.appendChild(s)};a("Library fonts",runtime.customFonts),a("System",SYSTEM_FONTS),a("Family",[t.props.font]);let o=Math.max(0,i.findIndex(e=>e.dataset.font===t.props.font));function s(e){i[o]?.classList.remove("active"),o=clamp(e,0,i.length-1);const t=i[o];t.classList.add("active"),n.setAttribute("aria-activedescendant",t.id),t.scrollIntoView({block:"nearest"})}function l(e){e!==t.props.font&&(pushUndo(),t.props.font=e,state.dirtyTimeline=!0,scheduleSave()),c(),renderInspector(),els.inspector.querySelector("[data-font-open]")?.focus({preventScroll:!0})}function c(){n.remove(),e.setAttribute("aria-expanded","false"),e.removeAttribute("aria-controls"),document.removeEventListener("pointerdown",d,!0),document.removeEventListener("scroll",u,!0),window.removeEventListener("resize",c),runtime.fontMenu=null}const d=t=>{n.contains(t.target)||e.contains(t.target)||c()},u=e=>{n.contains(e.target)||c()};n.addEventListener("keydown",t=>{if(t.stopPropagation(),"Tab"===t.key)return c(),void e.focus({preventScroll:!0});if(t.preventDefault(),"Escape"===t.key)c(),e.focus({preventScroll:!0});else if("ArrowDown"===t.key)s(o+1);else if("ArrowUp"===t.key)s(o-1);else if("Home"===t.key)s(0);else if("End"===t.key)s(i.length-1);else if("Enter"===t.key||" "===t.key)l(i[o].dataset.font);else if(1===t.key.length&&!t.ctrlKey&&!t.metaKey&&!t.altKey){const e=i.map((e,t)=>(o+t+1)%i.length).find(e=>i[e].dataset.font.toLowerCase().startsWith(t.key.toLowerCase()));void 0!==e&&s(e)}}),document.body.appendChild(n);const p=e.getBoundingClientRect();n.style.width=Math.min(Math.max(p.width,220),innerWidth-16)+"px",n.style.maxHeight=Math.min(330,innerHeight-16)+"px",n.style.left=clamp(p.left,8,Math.max(8,innerWidth-n.offsetWidth-8))+"px",n.style.top=clamp(p.bottom+4,8,Math.max(8,innerHeight-n.offsetHeight-8))+"px",e.setAttribute("aria-expanded","true"),e.setAttribute("aria-controls",n.id),runtime.fontMenu={close:c},n.focus({preventScroll:!0}),s(o),document.addEventListener("pointerdown",d,!0),document.addEventListener("scroll",u,!0),window.addEventListener("resize",c)}function closeFontPicker(){runtime.fontMenu?.close()}function closeStylePicker(){runtime.styleMenu&&runtime.styleMenu.close()}function replaceClipMedia(e,t){if(!t||e.mediaId===t.id)return;pushUndo();const n="video"===e.kind&&e.linkGroup?project.clips.filter(t=>t.linkGroup===e.linkGroup):[e];let i=!1;for(const e of n){if(e.mediaId=t.id,releaseClipEl(e.id),null==t.duration)continue;const n=e.props?.speed||1,r=Math.max(0,t.duration-.001);e.in>r&&(e.in=r,i=!0);const a=Math.max(.05,(t.duration-e.in)/n);e.duration>a&&(e.duration=a,i=!0)}"video"!==t.kind&&"audio"!==t.kind||ensureWave(t),"video"===e.kind&&reconcileAudioChannels(e),state.dirtyTimeline=!0,scheduleSave(),renderInspector(),toast(uiText(i?"Media replaced — trimmed to fit shorter source":"Media replaced"))}els.binList.addEventListener("click",e=>{(e.ctrlKey||e.metaKey)&&0===e.button&&(e.target.closest(".bin-del")||(e.preventDefault(),openFileImport(e.clientX,e.clientY)))});let replaceFileInput=null,replaceFileTargetId=null;function pickReplacementFile(e){replaceFileInput||(replaceFileInput=document.createElement("input"),replaceFileInput.type="file",replaceFileInput.accept=els.fileInput.accept,replaceFileInput.className="sr-only",document.body.appendChild(replaceFileInput),replaceFileInput.addEventListener("change",async()=>{const e=replaceFileInput.files;replaceFileInput.value="";const t=replaceFileTargetId;if(replaceFileTargetId=null,!e.length)return;const n=await importFiles(e),i=getClip(t);if(!i||!n.length)return;const r=n.find(e=>e.kind===i.kind)||n[0];r.kind===i.kind?replaceClipMedia(i,r):toast(uiText("Imported, but cannot replace {kind} with {replacement}",{kind:uiText(i.kind),replacement:uiText(r.kind)}))})),replaceFileTargetId=e.id,replaceFileInput.click()}function openMediaPicker(e,t){const n=project.media.filter(e=>e.kind===t.kind&&e.id!==t.mediaId),i=document.createElement("div");i.className="style-menu";const r=document.createElement("div");if(r.className="style-opt media-opt",r.textContent=uiText("📂 Browse file…"),r.addEventListener("click",()=>{o(),pickReplacementFile(t)}),i.appendChild(r),n.length){const e=document.createElement("div");e.style.cssText="height:1px;background:var(--border);margin:4px 2px;",i.appendChild(e);for(const e of n){const n=document.createElement("div");n.className="style-opt media-opt",n.textContent=e.name,n.title=e.name,n.addEventListener("click",()=>{o(),replaceClipMedia(t,e)}),i.appendChild(n)}}else{const e=document.createElement("div");e.className="style-opt media-opt",e.style.opacity=".6",e.style.cursor="default",e.textContent=uiText("No other {kind} in bin",{kind:uiText(t.kind)}),i.appendChild(e)}const a=t=>{i.contains(t.target)||t.target===e||o()};function o(){i.remove(),document.removeEventListener("pointerdown",a,!0),runtime.mediaMenu=null}document.addEventListener("pointerdown",a,!0);const s=e.getBoundingClientRect();i.style.left=Math.round(Math.min(s.left,innerWidth-220))+"px",i.style.top=Math.round(Math.min(s.bottom+4,innerHeight-340))+"px",document.body.appendChild(i),runtime.mediaMenu={close:o}}function closeMediaPicker(){runtime.mediaMenu&&runtime.mediaMenu.close()}function addTitle(){pushUndo();const e={id:"c_"+uid(),mediaId:null,kind:"text",track:"V2",start:state.time,in:0,duration:4,name:DEFAULT_PROPS.text,props:{...DEFAULT_PROPS}};runtime.titleStyleIdx=((runtime.titleStyleIdx||0)+1)%STYLE_CYCLE.length,applyTitleStyle(e,STYLE_CYCLE[runtime.titleStyleIdx]),e.props.textAnim="none",project.clips.push(e),selectClip(e.id),scheduleSave(),focusTextContent()}function addAdjust(){pushUndo();const e={id:"c_"+uid(),mediaId:null,kind:"adjust",track:"V3",start:state.time,in:0,duration:4,name:"Adjust",props:{...DEFAULT_PROPS}};project.clips.push(e),selectClip(e.id),scheduleSave()}function deleteRippleShifts(e){const t=new Set(e.map(e=>e.id)),n=new Map,i=new Set;for(const r of e)if(!i.has(r.id)){for(const e of withLinked([r]))i.add(e.id);for(const[e]of trimFollowers(r,r))t.has(e)||(n.has(e)||n.set(e,[]),n.get(e).push([r.start,clipEnd(r)]))}const r=new Map,a=new Map,o=new Map;for(const e of project.clips){if(t.has(e.id))continue;let i=0,s=-1/0;for(const[t,r]of(n.get(e.id)||[]).sort((e,t)=>e[0]-t[0]))i+=Math.max(0,r-Math.max(t,s)),s=Math.max(s,r);const l=e.linkGroup?`link:${e.linkGroup}`:`clip:${e.id}`;r.has(l)||r.set(l,{shift:1/0});const c=r.get(l);c.shift=Math.min(c.shift,i,e.start),a.set(e.id,c),o.has(e.track)||o.set(e.track,[]),o.get(e.track).push(e)}for(const e of r.values())e.desiredShift=e.shift;for(const e of o.values())e.sort((e,t)=>e.start-t.start);let s=!0;for(;s;){s=!1;for(const e of o.values())for(let t=1;t<e.length;t++){const n=e[t-1],i=e[t],r=a.get(i.id),o=a.get(n.id),l=Math.min(Math.max(0,i.start-clipEnd(n)),Math.max(0,r.desiredShift-o.desiredShift)),c=o.shift+l;r.shift>c+Timeline.EPS&&(r.shift=c,s=!0);const d=Math.max(0,r.shift-l);o.shift>d+Timeline.EPS&&(o.shift=d,s=!0)}}return new Map([...a].map(([e,t])=>[e,t.shift]))}function deleteSelected(){const e=withLinked(selectedClips());if(!e.length)return;pushUndo();const t=new Set(e.map(e=>e.id)),n=state.snap?deleteRippleShifts(e):new Map;for(const t of e)releaseClipEl(t.id);project.clips=project.clips.filter(e=>!t.has(e.id));for(const e of project.clips)e.start=Math.max(0,e.start-(n.get(e.id)||0));setSelection([]),scheduleSave(),renderInspector()}const GAP_EPS=1e-4;function gapAtPlayhead(e,t){const n=project.clips.filter(t=>t.track===e).sort((e,t)=>e.start-t.start);for(const e of n)if(e.start<=t&&t<clipEnd(e))return null;let i=0;for(const e of n)clipEnd(e)<=t&&(i=Math.max(i,clipEnd(e)));let r=1/0;for(const e of n)e.start>t&&(r=Math.min(r,e.start));return{gapStart:i,gapEnd:r}}function enabledTracks(){return TRACKS.filter(e=>isTrackEnabled(e.id))}function sharedGapAt(e){const t=enabledTracks();if(!t.length)return{reason:uiText("No enabled tracks")};let n=0,i=1/0;for(const r of t){const t=gapAtPlayhead(r.id,e);if(!t)return{reason:uiText("No shared gap: {track} contains a clip",{track:trackLabel(r.id)})};n=Math.max(n,t.gapStart),i=Math.min(i,t.gapEnd)}const r=i-n;if(!isFinite(i)||r<=1e-4)return{reason:uiText("Nothing to close at playhead")};const a=project.clips.filter(e=>isTrackEnabled(e.track)&&e.start>=i-1e-4);return a.length?{L:n,R:i,G:r,movers:a}:{reason:uiText("Nothing to close at playhead")}}function closeGapAtPlayhead(e=state.time){const t=sharedGapAt(e);if(t.reason)return void toast(t.reason);const{G:n,movers:i}=t;pushUndo();for(const e of i)e.start=Math.max(0,e.start-n);state.dirtyTimeline=!0,scheduleSave();const r=n>=1?n.toFixed(2):n.toFixed(3);toast(uiText("Closed {seconds}s gap",{seconds:r}))}function clearFocusedTransition(){if(!state.transFocus||!state.selId)return!1;const e=getClip(state.selId);if(!e)return!1;const t="in"===state.transFocus?"transitionIn":"transitionOut";return!!e[t]&&(pushUndo(),e[t]=void 0,state.transFocus=null,state.dirtyTimeline=!0,scheduleSave(),renderInspector(),!0)}function loadLastTransition(e){try{const t=JSON.parse(localStorage.getItem(LAST_TRANS_KEY[e])||"null");if(t?.type&&"none"!==t.type&&TRANSITIONS.includes(t.type)){const e=+t.duration;return{type:t.type,duration:isFinite(e)&&e>=MIN_TRANS_DUR?e:1}}}catch{}return{...DEFAULT_LAST_TRANS}}function saveLastTransition(e,t){if(t?.type&&"none"!==t.type)try{localStorage.setItem(LAST_TRANS_KEY[e],JSON.stringify({type:t.type,duration:Math.max(MIN_TRANS_DUR,+t.duration||1)}))}catch{}}function addTransitionAtPlayhead(){const e=getClip(state.selId);if(!e)return void toast(uiText("Select a clip first"));const t=state.time;if(t<e.start||t>=clipEnd(e))return void toast(uiText("Move playhead over the selected clip"));const n=(t-e.start)/e.duration<.5?"in":"out",i="in"===n?"transitionIn":"transitionOut",r=loadLastTransition(n),a=Math.min(Math.max(MIN_TRANS_DUR,r.duration),e.duration);pushUndo(),e[i]={type:r.type,duration:+a.toFixed(3)},saveLastTransition(n,e[i]),state.dirtyTimeline=!0,selectClip(e.id,{transFocus:n}),scheduleSave()}function gapSearchRange(){return null!=project.inPoint&&null!=project.outPoint?{t0:project.inPoint,t1:project.outPoint}:{t0:0,t1:Math.max(projDur(),0)}}function listAlignedGaps(e,t){const n=enabledTracks();if(!n.length||t-e<=1e-4)return[];const i=new Set([e,t]);for(const n of project.clips){if(!isTrackEnabled(n.track))continue;const r=n.start,a=clipEnd(n);r>e&&r<t&&i.add(r),a>e&&a<t&&i.add(a)}const r=[...i].sort((e,t)=>e-t),a=[],o=new Set;for(let i=0;i<r.length-1;i++){const s=r[i],l=r[i+1];if(l-s<=1e-4)continue;const c=(s+l)/2;let d=-1/0,u=1/0,p=!0;for(const e of n){const t=gapAtPlayhead(e.id,c);if(!t){p=!1;break}d=Math.max(d,t.gapStart),u=Math.min(u,t.gapEnd)}if(!p)continue;if(d=Math.max(d,e),u=Math.min(isFinite(u)?u:t,t),u-d<=1e-4)continue;const m=d.toFixed(5)+":"+u.toFixed(5);o.has(m)||(o.add(m),a.push({L:d,R:u}))}return a.sort((e,t)=>e.L-t.L),a}function ensurePlayheadVisible(){const e=state.time*state.pps,t=els.timelineScroll;t&&(e<t.scrollLeft||e>t.scrollLeft+t.clientWidth-40)&&(t.scrollLeft=Math.max(0,e-t.clientWidth/3))}function goToNextGap(){const{t0:e,t1:t}=gapSearchRange();if(t-e<=1e-4)return void toast(uiText("No gaps found"));const n=listAlignedGaps(e,t);if(!n.length)return void toast(null!=project.inPoint&&null!=project.outPoint?uiText("No gaps in IN/OUT range"):uiText("No gaps found"));const i=state.time;let r=n.find(e=>(e.L+e.R)/2>i+1e-4);r||(r=n[0]),setTime((r.L+r.R)/2),ensurePlayheadVisible()}function splitAtPlayhead(){const e=state.time;let t=state.selIds.size?selectedClips():project.clips;if(t=withLinked(t.filter(t=>e>t.start+.05&&e<clipEnd(t)-.05)),!t.length)return;pushUndo();const n=new Map;for(const i of t){const t=splitClipAt(i,e);t&&n.set(i.id,t)}relinkSplitRights(t,n),scheduleSave()}function splitClipAt(e,t){if(!(t>e.start+.05&&t<clipEnd(e)-.05))return null;const n=t-e.start,i={...e,id:"c_"+uid(),props:{...e.props},start:t,in:e.in+n*clipSpeed(e),duration:clipEnd(e)-t,keyframes:shiftKF(e.keyframes,n,clipEnd(e)-t),transitionIn:void 0,linkedId:void 0,linkGroup:void 0};return e.duration=n,e.keyframes=shiftKF(e.keyframes,0,n),e.transitionOut=void 0,project.clips.push(i),i}function relinkSplitRights(e,t){const n=new Map;for(const i of e){const e=t.get(i.id);if(e)if(i.linkGroup)n.has(i.linkGroup)||n.set(i.linkGroup,"lg_"+uid()),e.linkGroup=n.get(i.linkGroup);else{const n=i.linkedId?t.get(i.linkedId):null;n?(e.linkedId=n.id,n.linkedId=e.id):delete e.linkedId}}}function splitAtWorkArea(){const e=[project.inPoint,project.outPoint].filter(e=>null!=e);if(!e.length)return void toast(uiText("Set an IN or OUT marker first (I / O)"));const t=withLinked(project.clips.filter(t=>(e=>"function"!=typeof isTrackEnabled||isTrackEnabled(e.track))(t)&&e.some(e=>e>t.start+.05&&e<clipEnd(t)-.05)));if(t.length){pushUndo();for(const n of e.slice().sort((e,t)=>t-e)){const e=t.filter(e=>n>e.start+.05&&n<clipEnd(e)-.05),i=new Map;for(const t of e){const e=splitClipAt(t,n);e&&i.set(t.id,e)}relinkSplitRights(e,i)}scheduleSave()}else toast(uiText("Nothing to split at IN/OUT"))}function trimToPlayhead(e){const t=getClip(state.selId);if(!t)return;const n=state.time,i="in"===e&&n>t.start&&n<clipEnd(t)-.05,r="out"===e&&n>t.start+.05&&n<clipEnd(t);if(!i&&!r)return;const a={start:t.start,duration:t.duration};if(pushUndo(),i){const e=n-t.start;t.start=n,t.in="video"===t.kind||"audio"===t.kind?t.in+e*clipSpeed(t):0,t.duration-=e}else t.duration=n-t.start;for(const e of withLinked([t]))e.keyframes=shiftKF(e.keyframes,i?n-a.start:0,t.duration);syncLinkedTiming(t),rippleTrim(t,a),scheduleSave(),renderInspector()}function trimFollowers(e,t){const n=withLinked([e]),i=new Set(n.map(e=>e.id)),r=t.start+t.duration,a=new Map(n.map(e=>[e.track,r])),o=new Map;for(;;){const e=project.clips.filter(e=>!i.has(e.id)&&!o.has(e.id)&&a.has(e.track)&&e.start>=a.get(e.track)-1e-9);if(!e.length)return o;for(const t of withLinked(e))i.has(t.id)||o.has(t.id)||(o.set(t.id,t.start),a.set(t.track,Math.min(a.get(t.track)??1/0,t.start)))}}function rippleTrim(e,t,n=trimFollowers(e,t)){const i=e.duration-t.duration;e.start=t.start,syncLinkedTiming(e);for(const[e,t]of n){const n=getClip(e);n&&(n.start=Math.max(0,t+i))}}function trimToWorkArea(){const e=project.inPoint,t=project.outPoint;if(null==e&&null==t)return void toast(uiText("Set an IN or OUT marker first (I / O)"));const n=e=>"function"!=typeof isTrackEnabled||isTrackEnabled(e.track);if(!project.clips.some(i=>{if(!n(i))return!1;const r=i.start,a=clipEnd(i);let o=r,s=a;return null!=e&&(o=Math.max(o,e)),null!=t&&(s=Math.min(s,t)),s-o<.05||o>r+1e-6||s<a-1e-6}))return void toast(uiText("Nothing to trim"));pushUndo();const i=new Set;for(const r of project.clips){if(!n(r))continue;const a=r.start;let o=a,s=clipEnd(r);if(null!=e&&(o=Math.max(o,e)),null!=t&&(s=Math.min(s,t)),s-o<.05){i.add(r.id);continue}const l=o-a;l>1e-6&&(r.start=o,"video"===r.kind||"audio"===r.kind?r.in+=l*clipSpeed(r):r.in=0,r.duration-=l,r.keyframes=shiftKF(r.keyframes,l,r.duration),r.transitionIn=void 0),clipEnd(r)-s>1e-6&&(r.duration=Math.max(.05,s-r.start),r.keyframes=shiftKF(r.keyframes,0,r.duration),r.transitionOut=void 0)}for(const e of i)releaseClipEl(e);i.size&&(project.clips=project.clips.filter(e=>!i.has(e.id))),pruneSelection(),scheduleSave(),renderInspector()}function hasWorkArea(){return null!=project.inPoint||null!=project.outPoint}function playRange(){const e=null!=project.inPoint?project.inPoint:0,t=null!=project.outPoint?project.outPoint:Math.max(projDur(),0);return{start:e,end:Math.max(t,e)}}function playLimited(){return state.workAreaPlay&&!state.exporting&&hasWorkArea()}function playStopAt(e){const t=Math.max(e??projDur(),0);if(!playLimited())return t;const{end:n}=playRange();return state.time>n+1e-4?t:n}function gotoHome(){setTime(playLimited()&&null!=project.inPoint?project.inPoint:0)}function gotoEnd(){setTime(playLimited()&&null!=project.outPoint?project.outPoint:projDur())}function syncTrimIOButton(){const e=hasWorkArea(),t=$("btnTrimIO"),n=$("btnWorkAreaPlay");t&&t.classList.toggle("hidden",!e),n&&(n.classList.toggle("hidden",!e),n.classList.toggle("on",state.workAreaPlay))}function trackLabel(e){const t=TRACKS.find(t=>t.id===e)?.name;return t||uiText(e.startsWith("A")?"Audio {number}":"Video {number}",{number:e.slice(1)})}function trackIconKind(e){if(e.startsWith("A"))return"audio";const t=project.clips.filter(t=>t.track===e);return t.length&&t.every(e=>"text"===e.kind)?"text":"video"}function trackToggleIcon(e){return`<svg class="track-ico" data-kind="${e}" viewBox="0 0 16 16" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round">${"text"===e?'<path d="M1 4h6M4 4v9M9 4h6M12 4v9"/>':"audio"===e?'<circle cx="8" cy="8" r="6.4"/><path d="M9.5 4.3v6.1M9.5 4.3l2 .8M9.5 10.4c0 1.7-3.5 2.2-3.5.4 0-1.6 3.5-2.1 3.5-.4Z"/>':'<rect x="1.5" y="3" width="13" height="10" rx=".7"/><path d="m6.5 5.5 4 2.5-4 2.5Z" fill="currentColor" stroke="none"/>'}<path class="track-ico-off" d="M2 2l12 12"/></svg>`}function timelineVideoLabels(){const e=new Map(TRACKS.map((e,t)=>[e.id,t])),t=e=>state.clipTrimOrigins?.get(e.id)??e.start;return new Map(project.clips.filter(t=>"video"===t.kind&&e.has(t.track)).sort((n,i)=>t(n)-t(i)||e.get(n.track)-e.get(i.track)||n.id.localeCompare(i.id)).map((e,t)=>[e.id,uiText("Clip {number}",{number:t+1})]))}function buildTrackDOM(){els.tracks.dataset.signature=visibleTrackSignature(),els.trackHeaders.innerHTML="",els.tracks.innerHTML="";const e=document.createElement("div");e.id="trackHeadInner",els.trackHeaders.appendChild(e);for(const t of visibleTracks()){const n=isTrackEnabled(t.id),i=document.createElement("div");i.className="track-head"+(n?"":" disabled"),i.dataset.track=t.id,i.style.height=t.h+"px";const r=escapeHtml(trackLabel(t.id));i.innerHTML=`<button type="button" class="track-toggle" aria-pressed="${n}" title="${uiAttr(trackLabel(t.id))}" aria-label="${uiAttr(trackLabel(t.id))}">${trackToggleIcon(trackIconKind(t.id))}</button><span class="track-id sr-only">${r}</span>`,i.querySelector(".track-toggle").addEventListener("click",e=>{e.preventDefault(),e.stopPropagation(),toggleTrackEnabled(t.id)}),e.appendChild(i);const a=document.createElement("div");a.className="track"+(n?"":" disabled"),a.dataset.track=t.id,a.style.height=t.h+"px",els.tracks.appendChild(a)}if(!visibleTracks().length){const e=document.createElement("div");e.className="timeline-empty",e.textContent=uiText("Drop media here to create a track"),els.tracks.appendChild(e)}updateTimelineTrackLayout()}function updateTimelineTrackLayout(){if(state.gesture)return;const e=Math.max(0,els.timelineScroll.clientHeight-26),t=visibleTracks(),n=TRACK_SIZE_PRESETS[state.trackSize]||TRACK_SIZE_PRESETS.l,i=t.map(e=>n.h[e.id]??n.h["audio"===e.kind?"A1":"V1"]),r=i.reduce((e,t)=>e+t,0),a=r?Math.min(1,e/r):1,o=r?Math.max(0,(e-r*a)/2):0;t.forEach((e,t)=>{e.h=i[t]*a;const n=els.tracks.querySelector(`[data-track="${e.id}"]`),r=els.trackHeaders.querySelector(`[data-track="${e.id}"]`);for(const t of[n,r])t&&(t.style.height=e.h+"px")}),els.tracks.style.marginTop=o+"px";const s=$("trackHeadInner");s&&(s.style.transform=`translateY(${o}px)`)}function scrollTimeline(){els.timelineScroll.scrollLeft=0,els.timelineScroll.scrollTop=0,runtime.timelineScrollLeft=0}function contentWidth(){const e=Math.max(projDur()*state.pps+8,els.timelineScroll.clientWidth||800);return null!=state.clipGestureWidth?state.clipGestureWidth=Math.max(state.clipGestureWidth,e):e}function clipTransitionDur(e){if(!e||"none"===e.type)return 0;const t=+e.duration;return isFinite(t)&&t>0?t:0}function clipBorderRadius(){return"s"===state.trackSize?2:5}function transitionMarksHtml(e,t){let n="";const i=Math.max(8,t-6),r=clipBorderRadius(),a=(t,a)=>{const o=clipTransitionDur(t);if(!o)return;const s=Math.max(4,Math.min(o,e.duration)*state.pps),l=i-r,c=state.selId===e.id&&state.transFocus===a?" focused":"";n+="in"===a?`<div class="trans-mark in${c}" style="width:${s}px" data-side="in"><svg viewBox="0 0 ${s} ${i}" preserveAspectRatio="none" aria-hidden="true"><polygon points="0,0 ${s},0 0,${l}"/></svg><div class="trans-dur-handle" title="${uiAttr("Drag to adjust duration")}"></div></div>`:`<div class="trans-mark out${c}" style="width:${s}px" data-side="out"><svg viewBox="0 0 ${s} ${i}" preserveAspectRatio="none" aria-hidden="true"><polygon points="0,0 ${s},0 ${s},${l}"/></svg><div class="trans-dur-handle" title="${uiAttr("Drag to adjust duration")}"></div></div>`};return a(e.transitionIn,"in"),a(e.transitionOut,"out"),n}function clipKeyframeGroups(e){if(!e?.keyframes)return[];const t=new Map;for(const[n,i]of Object.entries(e.keyframes))if(Array.isArray(i))for(const e of i){const i=+e.t;if(!Number.isFinite(i))continue;const r=i.toFixed(4);let a=t.get(r);a||(a={t:i,keys:[]},t.set(r,a)),a.keys.includes(n)||a.keys.push(n)}return[...t.values()].sort((e,t)=>e.t-t.t)}const clipKeyframeLocalTimes=e=>clipKeyframeGroups(e).map(e=>e.t);function clipKeyframesHtml(e){const t=clipKeyframeGroups(e);if(!(t.length&&e.duration>0))return"";let n='<div class="clip-kfs">';for(const{t:i,keys:r}of t){if(i<-1e-6||i>e.duration+1e-6)continue;const t=Math.max(0,Math.min(100,i/e.duration*100)),a=r.map(e=>uiText(e[0].toUpperCase()+e.slice(1))).join(", ")+" @ "+fmt(e.start+i),o=r.length>1?`<span class="clip-kf-n">${r.length}</span>`:"";n+=`<div class="clip-kf${r.length>1?" multi":""}" style="left:${t}%" data-t="${i}" title="${a}">${o}</div>`}return n+"</div>"}function rebuildClips(){updateTimelineTrackLayout(),updateTimelineZoomControls(),visibleThumbKeys=new Set,neededThumbKeys=new Set,state.time>projDur()&&setTime(projDur()),els.playhead.style.left=state.time*state.pps+"px";const e=timelineVideoLabels(),t=contentWidth();els.tracksContent.style.width=t+"px",els.ruler.style.width=els.timelineScroll.clientWidth+"px";const n=state.gesture&&runtime.dropClipPreview,i=new Map(n?[...els.tracks.querySelectorAll(".clip")].map(e=>[e.dataset.id,e]):[]);if(!n)for(const e of els.tracks.children)e.dataset.track&&(e.innerHTML="");for(const t of project.clips){const n=trackOf(t);if(!n)continue;const r=els.tracks.querySelector(`[data-track="${t.track}"]`),a=i.get(t.id)||document.createElement("div");i.delete(t.id);const o=["drag-origin","reorder-preview"].filter(e=>a.classList.contains(e)),s=clipMediaFailure(t);a.className=`clip c-${t.kind}`+(s?" media-missing":"")+(state.selIds.has(t.id)?" selected":"")+(t.id===state.selId?" primary":"")+(o.length?" "+o.join(" "):""),a.dataset.id=t.id,a.title=s?mediaFailureLabel(s)+" · "+(getMedia(t.mediaId)?.name||t.name||""):t.name||"",a.style.left=t.start*state.pps+"px",a.style.width=Math.max(8,t.duration*state.pps)+"px";let l="";!s&&"video"===t.kind&&trackSizeShowsThumbs()&&r.offsetTop+n.h>=els.timelineScroll.scrollTop&&r.offsetTop<=els.timelineScroll.scrollTop+els.timelineScroll.clientHeight&&(l+=clipThumbStrip(t,n.h));const c=!s&&"audio"===t.kind&&!!wavePeaksFor(t);c&&(l+='<canvas class="wave"></canvas>');const d=(t.keyframes&&Object.keys(t.keyframes).length?"◆ ":"")+(t.transitionIn||t.transitionOut?"⇄ ":""),u=t.props?.audioChannel,p=0===u?"L · ":1===u?"R · ":Number.isInteger(u)?`Ch${u+1} · `:"",m="text"===t.kind?"T · "+(t.props.text||"").split("\n")[0]:"adjust"===t.kind?"FX · "+(t.name||""):"audio"===t.kind?p+(t.name||""):"video"===t.kind?e.get(t.id):t.name||"";l+=`<div class="fade"></div>\n <div class="clip-label">${s?"⚠ "+mediaFailureLabel(s)+" · ":""}${d}${escapeHtml(m)}</div>`,l+=clipKeyframesHtml(t);let f=`<div class="clip-body">${l}</div>`;f+=transitionMarksHtml(t,n.h),f+='<div class="handle l"></div><div class="handle r"></div>',a.innerHTML=f,c&&a.classList.add("has-wave"),a.parentElement!==r&&r.appendChild(a),c&&drawClipWave(a.querySelector(".wave"),t,n.h)}for(const e of i.values())e.remove();applyDropClipPreview(),preloadClipThumbs(),$("coverDialog").open&&renderCoverTimeline(),pruneClipThumbs(),loadClipThumbs(),state.dirtyTimeline=!1,updateWorkArea()}function drawClipWave(e,t,n){const i=wavePeaksFor(t);if(!(i instanceof Float32Array))return;const r=Math.min(2400,Math.max(8,Math.round(t.duration*state.pps))),a=n-8;e.width=r,e.height=a;const o=e.getContext("2d");o.fillStyle="#c9f29b",o.globalAlpha=.75;const s=a/2;for(let e=0;e<r;e++){const n=mediaTimeAt(t,t.start+e/r*t.duration),l=i[Math.floor(50*n)]||0,c=Math.max(1,l*(a-2));o.fillRect(e,s-c/2,1,c)}}let rulerWorker=null,rulerWorkerTried=!1;function ensureRulerWorker(){if(!rulerWorkerTried){rulerWorkerTried=!0;try{if(window.Worker&&els.ruler.transferControlToOffscreen){const e=els.ruler.transferControlToOffscreen(),t=new Worker("ruler-worker.js");t.postMessage({type:"init",canvas:e},[e]),rulerWorker=t}}catch{rulerWorker=null}}}function drawRuler(){ensureRulerWorker();const e=window.devicePixelRatio||1,t=els.timelineScroll.clientWidth;els.ruler.style.width=t+"px",els.ruler.style.height="26px",rulerWorker?rulerWorker.postMessage({type:"draw",w:t,h:26,dpr:e,sl:els.timelineScroll.scrollLeft,pps:state.pps,markers:project.markers,inPoint:project.inPoint,outPoint:project.outPoint,time:state.time,fps:project.fps,duration:projDur()}):drawRulerMainThread(t,26,e)}function drawRulerMainThread(e,t,n,i=els.ruler,r=els.timelineScroll.scrollLeft,a=state.pps,o=state.time,s=!0){i.width===e*n&&i.height===t*n||(i.width=e*n,i.height=t*n);const l=i.getContext("2d");l.setTransform(n,0,0,n,0,0),l.clearRect(0,0,e,t);const c=([.1,.25,.5,1,2,5,10,15,30,60,120,300].find(e=>e*a>=70)||600)/5,d=Math.max(0,Math.floor(r/a/c));l.strokeStyle="#4a4a55",l.fillStyle="#9a9aa6",l.font="10px Consolas, monospace",l.beginPath();const u=projDur();for(let n=d;n*c*a<r+e&&n*c<=u;n++){const e=n*c,i=Math.round(e*a-r)+.5,o=n%5==0;l.moveTo(i,o?8:17),l.lineTo(i,t),o&&l.fillText(fmt(Math.round(1e3*e)/1e3).slice(0,5),i+4,12)}l.stroke();const p=s?project.inPoint:null,m=s?project.outPoint:null;if(null!=p&&null!=m&&m>p){const n=p*a-r,i=m*a-r;l.fillStyle="#00000055",n>0&&l.fillRect(0,0,Math.min(e,n),t),i<e&&l.fillRect(Math.max(0,i),0,e-Math.max(0,i),t)}for(const n of(s?project.markers:[])||[]){const i=n.t*a-r;i<-6||i>e+6||(l.fillStyle="#ffd166",l.beginPath(),l.moveTo(i,t-9),l.lineTo(i+4,t-5),l.lineTo(i,t-1),l.lineTo(i-4,t-5),l.closePath(),l.fill())}const f=t-1,h=f-.75*(t-4),g=(h+f)/2;if(l.globalCompositeOperation="difference",null!=p){const t=p*a-r;t>=-10&&t<=e+10&&(l.fillStyle="#5eead4",l.beginPath(),l.moveTo(t,h),l.lineTo(t,f),l.lineTo(t+8,g),l.closePath(),l.fill())}if(null!=m){const t=m*a-r;t>=-10&&t<=e+10&&(l.fillStyle="#fb923c",l.beginPath(),l.moveTo(t,h),l.lineTo(t,f),l.lineTo(t-8,g),l.closePath(),l.fill())}l.globalCompositeOperation="source-over";const v=o*a-r;v>=-8&&v<=e+8&&(l.fillStyle="#ff4d6a",l.beginPath(),l.moveTo(v-6,12),l.lineTo(v+6,12),l.lineTo(v+6,19),l.lineTo(v,25),l.lineTo(v-6,19),l.closePath(),l.fill())}function snapTime(e,t,n=project.clips){if(!state.snap)return e;const i=t instanceof Set?t:new Set(t?[t]:[]),r=8/state.pps,a=[0,state.time];for(const e of project.markers||[])a.push(e.t);null!=project.inPoint&&a.push(project.inPoint),null!=project.outPoint&&a.push(project.outPoint);for(const e of n)i.has(e.id)||a.push(e.start,clipEnd(e));let o=e,s=r;for(const t of a){const n=Math.abs(t-e);n<s&&(s=n,o=t)}return o}function timeAtEvent(e){const t=els.timelineScroll.getBoundingClientRect();return clamp((e.clientX-t.left+els.timelineScroll.scrollLeft)/state.pps,0,1e6)}function trackAtEvent(e){for(const t of els.tracks.children){if(!t.dataset.track)continue;const n=t.getBoundingClientRect();if(e.clientY>=n.top&&e.clientY<n.bottom)return t.dataset.track}return null}function timelineClipHit(e){const t=e.target.closest(".clip");if(!t||e.target.closest(".clip-kf, .trans-mark"))return null;let n=getClip(t.dataset.id);if(!n)return null;const i=timeAtEvent(e);(i<n.start||i>=clipEnd(n))&&(n=project.clips.find(e=>e.track===n.track&&i>=e.start&&i<clipEnd(e))||n);const r=getClip(state.selId);if(!e.ctrlKey&&!e.metaKey&&!e.shiftKey&&r&&state.selIds.has(r.id)&&r.id!==n.id&&r.track===n.track){const e=Math.min(4,r.duration*state.pps/4,n.duration*state.pps/4),t=(i-clipEnd(r))*state.pps,a=(r.start-i)*state.pps;if(Math.abs(n.start-clipEnd(r))<Timeline.EPS&&t>=0&&t<=e)return{clip:r,mode:"trim-r"};if(Math.abs(clipEnd(n)-r.start)<Timeline.EPS&&a>=0&&a<=e)return{clip:r,mode:"trim-l"}}const a=(i-n.start)*state.pps,o=n.duration*state.pps,s=Math.min(8,o/4);return{clip:n,mode:a<=s?"trim-l":o-a<=s?"trim-r":"move"}}function showTrimHover(e){const t=e&&"move"!==e.mode?els.tracks.querySelector(`.clip[data-id="${CSS.escape(e.clip.id)}"] .handle.${"trim-l"===e.mode?"l":"r"}`):null,n=els.tracks.querySelector(".handle.trim-hover");n!==t&&(n?.classList.remove("trim-hover"),t?.classList.add("trim-hover"))}els.tracksContent.addEventListener("pointermove",e=>{state.gesture||showTrimHover(timelineClipHit(e))}),els.tracksContent.addEventListener("pointerleave",()=>showTrimHover(null)),els.timelineScroll.addEventListener("scroll",()=>showTrimHover(null)),window.addEventListener("blur",()=>showTrimHover(null));for(const e of["keydown","keyup"])window.addEventListener(e,e=>{["Control","Meta","Shift"].includes(e.key)&&showTrimHover(null)});els.tracksContent.addEventListener("pointerdown",e=>{if(0!==e.button)return;showTrimHover(null),isTypingTarget(document.activeElement)&&els.inspector.contains(document.activeElement)&&document.activeElement.blur();const t=e.target.closest(".clip");if(!t)return void startMarquee(e);let n=getClip(t.dataset.id);if(!n)return;const i=e.target.closest(".clip-kf");if(i)return e.preventDefault(),selectClip(n.id),void setTime(n.start+(+i.dataset.t||0));const r=e.target.closest(".trans-dur-handle");if(r){const t=r.closest(".trans-mark");return void startTransDurGesture(e,n,t?.classList.contains("out")?"out":"in")}const a=e.target.closest(".trans-mark");if(a)return e.preventDefault(),void selectClip(n.id,{transFocus:a.classList.contains("out")?"out":"in"});const o=timelineClipHit(e);if(!o)return;n=o.clip;const s=e.ctrlKey||e.metaKey||e.shiftKey;if(s){if(selectClip(n.id,{toggle:!0}),!state.selIds.has(n.id))return}else state.selIds.has(n.id)?state.selId!==n.id&&(state.selId=n.id,state.dirtyTimeline=!0,renderInspector()):selectClip(n.id);s||"move"!==o.mode||(setTime(n.start),runtime.followPlayhead=!0,lastTs=null),startClipGesture(e,n,o.mode,!s&&state.selIds.size>1)});const MIN_TRANS_DUR=.1;function startTransDurGesture(e,t,n){e.preventDefault();const i="in"===n?"transitionIn":"transitionOut",r=t[i];if(!r)return;selectClip(t.id,{transFocus:n}),state.gesture=!0;const a=r.duration,o=e.clientX;let s=!1;pushUndo();const l=e=>{const i=e.clientX-o;if(Math.abs(i)<2&&!s)return;s=!0;const l=clamp(a+("in"===n?1:-1)*i/state.pps,MIN_TRANS_DUR,t.duration);r.duration=+l.toFixed(3),state.dirtyTimeline=!0,rebuildClips();const c="in"===n?"transInDur":"transOutDur",d=els.inspector.querySelector(`[data-k="${c}"]`);d&&(d.value=r.duration)},c=()=>{window.removeEventListener("pointermove",l),window.removeEventListener("pointerup",c),state.gesture=!1,window.dispatchEvent(new Event("timelinegestureend")),s&&(scheduleSave(),saveLastTransition(n,t[i])),renderInspector()};window.addEventListener("pointermove",l),window.addEventListener("pointerup",c)}function timelineDropTarget(e,t){const n=[...els.tracks.children].filter(e=>e.dataset.track),i=n.filter(e=>TRACKS.find(t=>t.id===e.dataset.track)?.kind===t),r=els.tracksContent.getBoundingClientRect(),a=runtime.dropPreviewTarget;let o=e.clientY;if(a?.newTrack){const e=r.top+a.top;if(o>=e&&o<e+a.h)return a;o>=e+a.h&&(o-=a.h)}const s=e=>({top:r.top+e.offsetTop,bottom:r.top+e.offsetTop+e.offsetHeight,height:e.offsetHeight}),l=(TRACK_SIZE_PRESETS[state.trackSize]||TRACK_SIZE_PRESETS.s).h["audio"===t?"A1":"V1"];if(!i.length){const e=n.find(e=>"A"===e.dataset.track[0]),i=n.length?"video"===t&&e?e.offsetTop:(n[0]?.offsetTop||0)+els.tracks.offsetHeight:0;return{newTrack:!0,beforeId:"video"===t?e?.dataset.track:void 0,top:i,h:l}}for(const e of i){const t=s(e);if(Math.abs(o-t.top)<=5)return{newTrack:!0,beforeId:e.dataset.track,top:t.top-r.top,h:l};if(Math.abs(o-t.bottom)<=5){const i=n[n.indexOf(e)+1];return{newTrack:!0,beforeId:i?.dataset.track,top:t.bottom-r.top,h:l}}if(o>t.top&&o<t.bottom)return{trackId:e.dataset.track,top:t.top-r.top,h:t.height}}const c=i[i.length-1];return"audio"===t&&o>s(c).bottom?{newTrack:!0,top:c.offsetTop+c.offsetHeight,h:l}:null}function clearDropPreview(){runtime.dropPreviewTarget=null,runtime.dropClipPreview=null,els.tracks.style.paddingBottom="";for(const e of document.querySelectorAll(".drop-shift"))e.style.transform="",e.classList.remove("drop-shift");for(const e of els.tracksContent.querySelectorAll(".drop-preview"))e.remove();for(const e of els.tracks.querySelectorAll(".drag-origin"))e.classList.remove("drag-origin");for(const e of els.tracks.querySelectorAll(".reorder-preview"))e.style.transform="",e.classList.remove("reorder-preview")}function applyDropClipPreview(){const e=runtime.dropClipPreview;if(e){for(const t of els.tracks.querySelectorAll(".clip")){t.classList.toggle("drag-origin",e.ids.has(t.dataset.id));const n=e.shifts.get(t.dataset.id);(n||t.classList.contains("reorder-preview"))&&(t.classList.add("reorder-preview"),t.style.transform=`translateX(${(n||0)*state.pps}px)`)}for(const t of els.tracksContent.querySelectorAll(".drop-ghost[data-clip-id]")){const n=e.doc?.clips.find(e=>e.id===t.dataset.clipId),i=t.parentElement,r=els.timelineScroll,a=i.offsetTop+i.offsetHeight>=r.scrollTop&&i.offsetTop<=r.scrollTop+r.clientHeight,o=t.querySelector(".drop-thumb-strip"),s=n&&a&&trackSizeShowsThumbs()?clipThumbStrip({...n,start:+t.dataset.previewStart},i.offsetHeight):"";o.innerHTML!==s&&(o.innerHTML=s)}}}function createDropGhost(e,t,n,i){const r=document.createElement("div");if(r.className="drop-ghost",r.style.left=e*state.pps+"px",r.style.width=Math.max(8,t*state.pps)+"px","video"===i?.kind){r.classList.add("c-video"),r.dataset.clipId=i.id;const e=document.createElement("div");e.className="drop-thumb-strip",r.appendChild(e)}const a=document.createElement("span");return a.className="drop-ghost-label",a.textContent=n,r.appendChild(a),r.title=n,r}function showDropPreview(e,t,n,i=new Set,r,a,o){if(!e)return void clearDropPreview();els.tracks.style.paddingBottom="";for(const e of document.querySelectorAll(".drop-shift"))e.style.transform="",e.classList.remove("drop-shift");if(runtime.dropPreviewTarget=e,e.newTrack){els.tracks.style.paddingBottom=e.h+"px";for(const t of els.tracks.children){if(!t.dataset.track||t.offsetTop<e.top-1)continue;const n=els.trackHeaders.querySelector(`[data-track="${t.dataset.track}"]`);for(const i of[t,n])i&&(i.classList.add("drop-shift"),i.style.transform=`translateY(${e.h}px)`)}}const s=Number.isFinite(o),l=s?o-t:0,c=s?timelineVideoLabels():new Map,d=e=>e?c.get(e.id)||e.name||("text"===e.kind?(e.props?.text||"").split("\n")[0]:"")||e.id:"",u=new Map([...els.tracksContent.querySelectorAll(".drop-preview")].map(e=>[e.dataset.previewId,e])),p=(t,n,i,r,a,o,c,d=!1)=>{let p=u.get(t);p||(p=document.createElement("div"),p.dataset.previewId=t,p.appendChild(createDropGhost(n,i,r,a)),els.tracksContent.appendChild(p)),u.delete(t),p.className="drop-preview"+(d?" linked-drop-preview":e.newTrack?" new-track":"")+(s?" moving":""),p.style.top=Math.max(0,o)+"px",p.style.height=c+"px";const m=p.querySelector(".drop-ghost");m.classList.toggle("companion",d),m.style.left=(n+l)*state.pps+"px",m.style.width=Math.max(8,i*state.pps)+"px",m.dataset.previewStart=n+l;const f=m.querySelector(".drop-ghost-label");if(f.textContent!==r&&(f.textContent=r),s?m.removeAttribute("title"):m.title=r,s){let e=p.querySelector(".drop-slot");e||(e=document.createElement("div"),e.className="drop-slot",p.prepend(e)),e.style.left=n*state.pps+"px",e.style.width=Math.max(8,i*state.pps)+"px"}return p},m=r?.clips.find(e=>e.id===a),f=p("primary",t,n,s?d(m):dropPlacementLabel(e,t),m,e.top,e.h);if(s){let n=f.querySelector(".drop-insertion");n||(n=document.createElement("div"),n.className="drop-insertion",f.appendChild(n));const a=(r?.clips||[]).filter(t=>i.has(t.id)&&t.track===e.trackId);n.style.left=Math.min(t,...a.map(e=>e.start))*state.pps+"px"}if(r)for(const t of r.clips){if(!i.has(t.id)||t.id===a)continue;const n=els.tracks.querySelector(`[data-track="${t.track}"]`);n&&p("linked:"+t.id,t.start,t.duration,s?d(t):uiText("Moves together"),t,n.offsetTop+(e.newTrack&&n.offsetTop>=e.top-1?e.h:0),n.offsetHeight,!0)}for(const e of u.values())e.remove();const h=new Map;if(r)for(const e of r.clips){const t=getClip(e.id);t&&!i.has(e.id)&&e.start!==t.start&&h.set(e.id,e.start-t.start)}runtime.dropClipPreview={ids:i,shifts:h,doc:r},applyDropClipPreview()}function dropPlacementLabel(e,t){return e.newTrack?uiText("+ New track"):uiText("Move to {track} · {seconds}s",{track:trackLabel(e.trackId),seconds:t.toFixed(2)})}function showTimelineHint(e,t,n=!1){const i=$("timelineGestureHint");i.textContent=e,i.hidden=!1,i.classList.toggle("limited",n),i.style.left=clamp(t.clientX+14,8,Math.max(8,window.innerWidth-i.offsetWidth-8))+"px",i.style.top=clamp(t.clientY-i.offsetHeight-16,8,Math.max(8,window.innerHeight-i.offsetHeight-8))+"px"}function hideTimelineHint(){$("timelineGestureHint").hidden=!0,delete els.tracksContent.dataset.gesture}function showTrimHint(e,t,n,i,r){const a=e.duration-t.duration;showTimelineHint(`${uiText(a<0?"Trimmed {seconds}s":"Restored {seconds}s",{seconds:Math.abs(a).toFixed(2)})} · ${uiText("Duration {seconds}s",{seconds:e.duration.toFixed(2)})}\n${n.size?uiText("Following clips move together"):uiText("Linked media stays in sync")}`+(r?" · "+uiText("Source or clip limit reached"):""),i,r)}function closeTimelineMenu(){runtime.timelineMenu?.close()}function highlightTimelineGap(e){for(const e of els.tracksContent.querySelectorAll(".timeline-gap-preview"))e.remove();if(e&&!e.reason)for(const t of els.tracks.children){if(!t.dataset.track||!isTrackEnabled(t.dataset.track))continue;const n=document.createElement("div");n.className="timeline-gap-preview",Object.assign(n.style,{left:e.L*state.pps+"px",width:e.G*state.pps+"px",top:t.offsetTop+"px",height:t.offsetHeight+"px"}),els.tracksContent.appendChild(n)}}function openTimelineMenu(e,t){if(closeTimelineMenu(),state.gesture||state.exporting)return;e.preventDefault(),state.playing&&pause(),t&&(state.selIds.has(t.id)?state.selId!==t.id&&setSelection([...state.selIds],t.id):selectClip(t.id));const n=state.time,i=timeAtEvent(e),r=timelineSnapshot(),a=[...state.selIds].join("\0"),o=document.activeElement,s=t?null:sharedGapAt(i),l=document.createElement("div");l.className="ctx-menu timeline-menu",l.setAttribute("role","menu"),l.setAttribute("aria-label",uiText(t?"Clip actions":"Timeline actions"));const c=document.createElement("div");c.className="timeline-menu-heading",c.textContent=t?uiText("Selected {count} clips · linked media follows",{count:state.selIds.size}):uiText("Shared gap on enabled tracks"),l.appendChild(c);const d=[],u=(e,t,n,i,r,a,o)=>d.push({key:e,label:t,shortcut:n,enabled:i,reason:r,run:a,highlight:o});if(t){const e=selectedClips().some(e=>n>e.start+.05&&n<clipEnd(e)-.05),i=uiText("Move the playhead inside the clip first");u("start",uiText("Go to clip start"),"",!0,"",()=>{setTime(t.start),ensurePlayheadVisible()}),u("selection",uiText("Zoom to selected clips"),"Z",!0,"",zoomToSelection),u("split",uiText("Split at playhead"),"S",e,i,splitAtPlayhead),u("trim-in",uiText("Trim start to playhead"),"[",n>t.start&&n<clipEnd(t)-.05,i,()=>trimToPlayhead("in")),u("trim-out",uiText("Trim end to playhead"),"]",n>t.start+.05&&n<clipEnd(t),i,()=>trimToPlayhead("out")),u("delete",uiText("Delete selected clips"),"Del",!0,"",deleteSelected)}else u("gap",uiText("Close shared gap")+(s.reason?"":` · ${s.G.toFixed(2)}s`),"⇧G",!s.reason,s.reason,()=>closeGapAtPlayhead(i),s),u("next-gap",uiText("Find gap"),"G",!0,"",goToNextGap),u("fit",uiText("Fit timeline"),"⇧Z",!0,"",zoomToFit);u("help",uiText("Keyboard shortcuts"),"",!0,"",()=>$("helpOverlay").classList.remove("hidden"));for(const e of d){const t=document.createElement("button");t.type="button",t.className="ctx-opt",t.dataset.action=e.key,t.setAttribute("role","menuitem"),t.disabled=!e.enabled,t.title=e.enabled?e.label:e.reason;const i=document.createElement("span");if(i.textContent=e.label,t.appendChild(i),e.shortcut){const n=document.createElement("kbd");n.textContent=e.shortcut,t.appendChild(n)}if(!e.enabled){const n=document.createElement("small");n.textContent=e.reason,t.appendChild(n)}t.addEventListener("pointerenter",()=>highlightTimelineGap(e.enabled?e.highlight:null)),t.addEventListener("focus",()=>highlightTimelineGap(e.enabled?e.highlight:null)),t.addEventListener("click",()=>{const t=timelineSnapshot()!==r||[...state.selIds].join("\0")!==a;closeTimelineMenu(),t?toast(uiText("Timeline changed. Open the menu again.")):(["split","trim-in","trim-out"].includes(e.key)&&setTime(n),e.run())}),l.appendChild(t)}const p=e=>{l.contains(e.target)||closeTimelineMenu()};runtime.timelineMenu={close:()=>{runtime.timelineMenu=null;const e=l.contains(document.activeElement);l.remove(),highlightTimelineGap(null),document.removeEventListener("pointerdown",p,!0),els.timelineScroll.removeEventListener("scroll",closeTimelineMenu),window.removeEventListener("resize",closeTimelineMenu),e&&o?.isConnected&&o.focus({preventScroll:!0})}},l.addEventListener("keydown",e=>{if(e.stopPropagation(),"Escape"===e.key)return e.preventDefault(),void closeTimelineMenu();const t=[...l.querySelectorAll("button:not(:disabled)")],n=t.indexOf(document.activeElement),i="ArrowDown"===e.key?(n+1)%t.length:"ArrowUp"===e.key?(n-1+t.length)%t.length:"Home"===e.key?0:"End"===e.key?t.length-1:-1;i>=0&&(e.preventDefault(),t[i].focus()),"Tab"===e.key&&closeTimelineMenu()}),l.addEventListener("pointerleave",()=>highlightTimelineGap(null)),document.body.appendChild(l),l.style.left=clamp(e.clientX,8,Math.max(8,window.innerWidth-l.offsetWidth-8))+"px",l.style.top=clamp(e.clientY,8,Math.max(8,window.innerHeight-l.offsetHeight-8))+"px",l.querySelector("button:not(:disabled)")?.focus(),document.addEventListener("pointerdown",p,!0),els.timelineScroll.addEventListener("scroll",closeTimelineMenu),window.addEventListener("resize",closeTimelineMenu)}function movePlacement(e,t,n,i,r,a){const o=structuredClone(e),s=o.clips.find(e=>e.id===t),l=o.clips.filter(e=>n.has(e.id));if(!i.newTrack&&i.trackId===s.track&&Number.isFinite(a)){const e=Math.max(r-s.start,-Math.min(...l.map(e=>e.start))),t=sameTrackMoveDelta(o.clips,l,n,e);if(Math.abs(t-e)>Timeline.EPS&&reorderPlacement(o.clips,s,n,a))return{doc:o,start:s.start};for(const e of l)e.start+=t;return{doc:o,start:s.start}}i.newTrack?s.track=Timeline.insertTrack(o,Timeline.kindOf(s),i.beforeId):s.track=i.trackId;let c=Math.max(r-s.start,-Math.min(...l.map(e=>e.start)));const d=l.filter(e=>e===s||"audio"!==e.kind);let u;do{u=!1;for(const e of d){const t=Timeline.firstGap(o.clips,e.track,e.start+c,e.duration,n);t>e.start+c+Timeline.EPS&&(c=t-e.start,u=!0)}}while(u);for(const e of l)e.start+=c;const p=new Set(n);for(const e of l)p.delete(e.id),"audio"===e.kind&&e!==s&&(e.track=Timeline.freeTrack(o,"audio",e.start,e.duration,e.track,p));return Timeline.normalize(o),{doc:o,start:s.start}}function sameTrackMoveDelta(e,t,n,i){const r=[];for(const i of t)for(const t of e)n.has(t.id)||t.track!==i.track||r.push([t.start-i.start-i.duration,clipEnd(t)-i.start]);r.sort((e,t)=>e[0]-t[0]);const a=[];for(const e of r){const t=a[a.length-1];t&&e[0]<t[1]-Timeline.EPS?t[1]=Math.max(t[1],e[1]):a.push(e)}for(const[e,t]of a)if(i>e+Timeline.EPS&&i<t-Timeline.EPS)return i>0?Math.max(0,e):Math.min(0,t);return i}function reorderPlacement(e,t,n,i){const r=e.filter(e=>e.track===t.track).sort((e,t)=>e.start-t.start),a=r.filter(e=>n.has(e.id)),o=r.indexOf(a[0]),s=t=>{const n=new Set(t.map(e=>e.linkGroup).filter(Boolean)),i=new Set(t.flatMap(e=>[e.id,e.linkedId]).filter(Boolean));return e.filter(e=>n.has(e.linkGroup)||i.has(e.id)||i.has(e.linkedId))};if(r.indexOf(a[a.length-1])-o+1!==a.length)return!1;const l=new Set(s(a).map(e=>e.id));if([...n].some(e=>!l.has(e)))return!1;if(r.some((e,t)=>t&&e.start<clipEnd(r[t-1])-Timeline.EPS))return!1;const c=r.filter(e=>!n.has(e.id)),d=c.filter(e=>i>=e.start+e.duration/2).length;if(d===o)return!1;const u=d<o,p=u?c.slice(d,o):c.slice(o,d),m=a[0].start,f=clipEnd(a[a.length-1]),h=f-m,g=d?clipEnd(c[d-1]):c[0].start,v=d<c.length?c[d].start:g,x=Math.abs(i-g)<=Math.abs(i-v)?g:v,b=new Map([...n].map(e=>[e,x-(u?m:f)]));for(const e of s(p)){const t=u?h:-h;if(b.has(e.id)&&Math.abs(b.get(e.id)-t)>Timeline.EPS)return!1;b.set(e.id,t)}const y=new Map(e.map(e=>[e.id,e.start+(b.get(e.id)||0)]));for(const t of e){if(!b.has(t.id))continue;const n=y.get(t.id);if(n<-Timeline.EPS)return!1;for(const i of e)if(i.id!==t.id&&i.track===t.track&&n<y.get(i.id)+i.duration-Timeline.EPS&&n+t.duration>y.get(i.id)+Timeline.EPS)return!1}for(const t of e)b.has(t.id)&&(t.start=Math.max(0,y.get(t.id)));return!0}function trimGestureBounds(e,t){const n=new Set(t.map(e=>e.id));let i=-1/0,r=1/0;for(const e of t){const t=clipSpeed(e),a=getMedia(e.mediaId),o="video"===e.kind||"audio"===e.kind;i=Math.max(i,-e.start,o?-(e.in||0)/t:-1/0),o&&a?.duration&&(r=Math.min(r,(a.duration-(e.in||0))/t-e.duration));for(const t of project.clips)n.has(t.id)||t.track!==e.track||(clipEnd(t)<=e.start+Timeline.EPS&&(i=Math.max(i,clipEnd(t)-e.start)),t.start>=clipEnd(e)-Timeline.EPS&&(r=Math.min(r,t.start-clipEnd(e))))}return{left:i,right:r}}function updateClipDragScroll(e){const t=runtime.clipDrag;if(!t||!state.gesture||document.hidden)return;const n=els.timelineScroll,i=n.getBoundingClientRect(),r=n.clientWidth,a=t.pointer;if(!r||a.clientY<i.top+n.clientTop||a.clientY>=i.top+n.clientTop+n.clientHeight)return;const o=clamp(a.clientX-i.left-n.clientLeft,0,r),s=Math.min(48,r/4),l=o<s?(o-s)/s:o>r-s?(o-r+s)/s:0;if(!l)return;const c=Math.max(0,Math.min(n.scrollWidth,state.clipGestureWidth)-r),d=clamp(n.scrollLeft+720*l*e,0,c);Math.abs(d-n.scrollLeft)<.01||(runtime.followPlayhead=!1,scrollTimeline(d),t.update(a))}function startClipGesture(e,t,n,i){e.preventDefault(),closeTimelineMenu(),state.gesture=!0,els.tracksContent.dataset.gesture=n,"move"===n&&($("timelineGestureHint").hidden=!0),state.clipGestureWidth=els.tracksContent?.offsetWidth||0;const r={start:t.start,in:t.in,duration:t.duration,track:t.track,keyframes:t.keyframes?JSON.parse(JSON.stringify(t.keyframes)):void 0},a=withLinked("move"===n&&state.selIds.has(t.id)?selectedClips():[t]),o=new Map(a.map(e=>[e.id,{start:e.start,in:e.in,duration:e.duration,keyframes:e.keyframes?JSON.parse(JSON.stringify(e.keyframes)):void 0}])),s=new Set(a.map(e=>e.id)),l="move"===n?null:trimFollowers(t,r),c=project.clips.map(e=>({id:e.id,start:e.start,duration:e.duration}));state.clipTrimOrigins="trim-l"===n?new Map(c.map(e=>[e.id,e.start])):null;let d=1e6,u=1e6;if("move"!==n){const e=clipSpeed(t);for(const t of a){if("video"!==t.kind&&"audio"!==t.kind)continue;d=Math.min(d,r.in/e);const n=getMedia(t.mediaId);n?.duration&&(u=Math.min(u,(n.duration-r.in)/e))}const n=new Set(a.map(e=>e.track)),i=r.start+r.duration;for(const e of project.clips)!s.has(e.id)&&n.has(e.track)&&e.start<i-1e-9&&clipEnd(e)>=i&&(u=Math.min(u,r.duration),d=0)}const p=els.timelineScroll.scrollLeft,m=e.clientX,f=e.clientY,h="move"===n?timeAtEvent(e):0;let g=!1,v=!1;const x=els.tracksContent,b=timelineSnapshot(),y=JSON.parse(b);let T=null;const k=i=>{if(null!=e.pointerId&&null!=i.pointerId&&i.pointerId!==e.pointerId)return;const v=(i.clientX-m+els.timelineScroll.scrollLeft-p)/state.pps;if(!g&&("move"===n?Math.hypot(i.clientX-m,i.clientY-f)>6:Math.abs(i.clientX-m)>3||Math.abs(i.clientY-f)>3)&&(g=!0,"move"===n&&state.playing&&pause()),g){if("move"===n){const e=timelineDropTarget(i,Timeline.kindOf(t));if(!e)return runtime.clipDrag=null,T=null,void clearDropPreview();runtime.clipDrag={pointer:{clientX:i.clientX,clientY:i.clientY,pointerId:i.pointerId},update:k};const n=r.start+v,l=n+r.duration,d=snapTime(n,s,c),u=snapTime(l,s,c),p=Math.abs(d-n),m=Math.abs(u-l),f=m>0&&(0===p||m<p)?u-r.duration:d,g=h+v;T={...movePlacement(y,t.id,s,e,f,g),target:e,proposedStart:f,pointerTime:g};const x=Math.max(n,r.start-Math.min(...a.map(e=>o.get(e.id).start)));showDropPreview(e,T.start,r.duration,s,T.doc,t.id,x)}else if("trim-l"===n){let e=snapTime(r.start+v,s,c);const n=clipSpeed(t);e=clamp(e,r.start-d,r.start+Math.max(0,r.duration-.05));const u=e-r.start;t.start=Math.max(0,e),t.in="video"===t.kind||"audio"===t.kind?r.in+u*n:0,t.duration=r.duration-u;for(const e of a)e.keyframes=shiftKF(o.get(e.id).keyframes,u,t.duration);syncLinkedTiming(t);for(const[t,n]of l){const i=getClip(t);i&&(i.start=n-Math.min(0,e))}showTrimHint(t,r,l,i,Math.abs(e-snapTime(r.start+v,s,c))>Timeline.EPS)}else{const e=snapTime(r.start+r.duration+v,s,c);t.duration=clamp(e-r.start,.05,u);for(const e of a)e.keyframes=shiftKF(o.get(e.id).keyframes,0,t.duration);rippleTrim(t,r,l),showTrimHint(t,r,l,i,Math.abs(t.duration-(e-r.start))>Timeline.EPS)}"move"!==n&&(state.dirtyTimeline=!0,renderInspector(!0))}},S=(a,o={})=>{if(!(v||null!=e.pointerId&&null!=o.pointerId&&o.pointerId!==e.pointerId)){if(v=!0,runtime.clipDrag=null,window.removeEventListener("pointermove",k),window.removeEventListener("pointerup",w),window.removeEventListener("pointercancel",E),window.removeEventListener("keydown",C,!0),window.removeEventListener("blur",M),x.removeEventListener("lostpointercapture",M),x.hasPointerCapture(e.pointerId)&&x.releasePointerCapture(e.pointerId),clearDropPreview(),hideTimelineHint(),!a&&g&&"trim-l"===n&&rippleTrim(t,r,l),state.clipTrimOrigins=null,state.gesture=!1,state.clipGestureWidth=null,a)"move"!==n&&restoreTimeline(b);else if(g&&("move"!==n||T)){let e=b;if(T&&(e=timelineSnapshot(),getClip(t.id))){const n=new Set(withLinked(project.clips.filter(e=>s.has(e.id))).map(e=>e.id)),i=movePlacement(JSON.parse(e),t.id,n,T.target,T.proposedStart,T.pointerTime);restoreTimeline(JSON.stringify(i.doc))}timelineSnapshot()!==e&&(runtime.undo.push(e),runtime.undo.length>100&&runtime.undo.shift(),runtime.redo.length=0,scheduleSave())}else!g&&i&&selectClip(t.id);state.dirtyTimeline=!0,renderInspector(),window.dispatchEvent(new Event("timelinegestureend")),runtime.pendingSync&&syncFromServer()}},w=e=>S(!1,e),E=e=>S(!0,e),M=e=>S(!1,e),C=e=>{"Escape"===e.key&&(e.preventDefault(),e.stopImmediatePropagation(),S(!0))};window.addEventListener("pointermove",k),window.addEventListener("pointerup",w),window.addEventListener("pointercancel",E),window.addEventListener("keydown",C,!0),window.addEventListener("blur",M),x.addEventListener("lostpointercapture",M);try{x.setPointerCapture(e.pointerId)}catch{}}function startMarquee(e){e.preventDefault(),state.gesture=!0;const t=e.ctrlKey||e.metaKey||e.shiftKey?new Set(state.selIds):new Set,n=els.tracksContent.getBoundingClientRect(),i=e.clientX-n.left,r=e.clientY-n.top,a=new Map;for(const e of els.tracks.children)a.set(e.dataset.track,{top:e.offsetTop,h:e.offsetHeight});let o=null,s=!1;const l=e=>{const n=els.tracksContent.getBoundingClientRect(),l=e.clientX-n.left,c=e.clientY-n.top;if(!s&&Math.hypot(l-i,c-r)<4)return;s=!0,o||(o=document.createElement("div"),o.className="marquee",els.tracksContent.appendChild(o));const d=Math.min(i,l),u=Math.min(r,c),p=Math.abs(l-i),m=Math.abs(c-r);Object.assign(o.style,{left:d+"px",top:u+"px",width:p+"px",height:m+"px"});const f=new Set(t);for(const e of project.clips){const t=a.get(e.track);if(!t)continue;const n=e.start*state.pps,i=n+Math.max(8,e.duration*state.pps);n<d+p&&i>d&&t.top<u+m&&t.top+t.h>u&&f.add(e.id)}state.selIds=f;for(const e of els.tracks.querySelectorAll(".clip"))e.classList.toggle("selected",f.has(e.dataset.id))},c=e=>{window.removeEventListener("pointermove",l),window.removeEventListener("pointerup",c),state.gesture=!1,window.dispatchEvent(new Event("timelinegestureend")),o&&o.remove(),s?(state.selIds.has(state.selId)||(state.selId=[...state.selIds].pop()??null),state.dirtyTimeline=!0,renderInspector(),syncBinSelectionFromTimeline()):(selectClip(null),setTime(timeAtEvent(e))),runtime.pendingSync&&syncFromServer()};window.addEventListener("pointermove",l),window.addEventListener("pointerup",c)}function updateScrub(e=0){if(!runtime.scrub)return;const t=els.timelineScroll,n=t.clientWidth;if(!n)return;const i=clamp(runtime.scrub.clientX-t.getBoundingClientRect().left-t.clientLeft,0,n-1),r=Math.min(48,n/4),a=i<r?(i-r)/r:i>n-r?(i-n+r)/r:0,o=Math.min(t.scrollWidth-n,Math.max(0,Math.ceil(projDur()*state.pps-i))),s=720*a*e;s<0?t.scrollLeft=Math.max(0,t.scrollLeft+s):s>0&&t.scrollLeft<o&&(t.scrollLeft=Math.min(o,t.scrollLeft+s));const l=clamp((t.scrollLeft+i)/state.pps,0,projDur());l!==state.time&&setTime(l)}function startScrub(e){if(0!==e.button||runtime.scrub)return;e.preventDefault(),e.stopPropagation(),state.playing&&pause(),runtime.followPlayhead=!0,state.gesture=!0,runtime.scrub={clientX:e.clientX},silencePreviewMedia();const t=t=>{t.pointerId===e.pointerId&&(runtime.scrub.clientX=t.clientX,updateScrub())};updateScrub();const n=i=>{null!=i.pointerId&&i.pointerId!==e.pointerId||(window.removeEventListener("pointermove",t),window.removeEventListener("pointerup",n),window.removeEventListener("pointercancel",n),window.removeEventListener("blur",n),els.ruler.removeEventListener("lostpointercapture",n),els.ruler.hasPointerCapture(e.pointerId)&&els.ruler.releasePointerCapture(e.pointerId),runtime.scrub=null,state.gesture=!1,window.dispatchEvent(new Event("timelinegestureend")),runtime.pendingSync&&syncFromServer())};window.addEventListener("pointermove",t),window.addEventListener("pointerup",n),window.addEventListener("pointercancel",n),window.addEventListener("blur",n),els.ruler.addEventListener("lostpointercapture",n);try{els.ruler.setPointerCapture(e.pointerId)}catch{}}function setTime(e){resetPreviewWait(!0),state.time=clamp(e,0,Math.max(projDur(),0)),seekMediaWhilePaused(),state.audioHold&&scheduleAudioHoldRefresh()}function keyframeTimelineTimes(e){const t=new Set,n=[];for(const i of e)for(const e of clipKeyframeLocalTimes(i)){const r=+(i.start+e).toFixed(4);t.has(r)||(t.add(r),n.push(r))}return n.sort((e,t)=>e-t),n}function goToKeyframe(e){let t=state.selIds.size?selectedClips():[];if(!t.some(e=>clipKeyframeLocalTimes(e).length)){const e=state.time;t=project.clips.filter(t=>clipKeyframeLocalTimes(t).length&&e>=t.start-1e-6&&e<=t.start+t.duration+1e-6)}const n=keyframeTimelineTimes(t);if(!n.length)return void toast(uiText("No keyframes"));const i=.5/Math.max(1,project.fps||30);if(e>0){const e=n.find(e=>e>state.time+i);if(null==e)return void toast(uiText("No next keyframe"));setTime(e)}else{let e=null;for(const t of n)t<state.time-i&&(e=t);if(null==e)return void toast(uiText("No previous keyframe"));setTime(e)}}function toggleMarker(){const e=+state.time.toFixed(3),t=Math.max(.05,8/state.pps);project.markers=project.markers||[];const n=project.markers.findIndex(n=>Math.abs(n.t-e)<t);n>=0&&!state.playing?project.markers.splice(n,1):(project.markers.push({t:e}),project.markers.sort((e,t)=>e.t-t.t)),scheduleSave()}function workAreaTime(){return Math.max(0,+state.time.toFixed(3))}function setInPoint(){const e=workAreaTime(),t=project.outPoint,{inPoint:n,outPoint:i}=normalizeWorkArea(e,t);null!=t&&null==n&&null==i&&toast(Math.abs(e-t)<1e-6?uiText("IN and OUT must be at different times"):uiText("IN must be before OUT")),project.inPoint=n,project.outPoint=i,updateWorkArea(),syncTrimIOButton(),scheduleSave()}function setOutPoint(){const e=workAreaTime(),t=project.inPoint,{inPoint:n,outPoint:i}=normalizeWorkArea(t,e);null!=t&&null==n&&null==i&&toast(Math.abs(e-t)<1e-6?uiText("IN and OUT must be at different times"):uiText("OUT must be after IN")),project.inPoint=n,project.outPoint=i,updateWorkArea(),syncTrimIOButton(),scheduleSave()}function clearInPoint(){null!=project.inPoint&&(project.inPoint=null,updateWorkArea(),syncTrimIOButton(),scheduleSave())}function clearOutPoint(){null!=project.outPoint&&(project.outPoint=null,updateWorkArea(),syncTrimIOButton(),scheduleSave())}function updateWorkArea(){const e=$("workDimL"),t=$("workDimR");if(!e||!t)return;const n=project.inPoint,i=project.outPoint,r=els.tracksContent.offsetWidth||contentWidth();if(null==n||null==i||i<=n)return e.classList.add("hidden"),void t.classList.add("hidden");const a=clamp(n*state.pps,0,r),o=clamp(i*state.pps,0,r);e.classList.remove("hidden"),t.classList.remove("hidden"),e.style.width=Math.max(0,a)+"px",t.style.left=o+"px",t.style.width=Math.max(0,r-o)+"px"}els.tracksContent.addEventListener("contextmenu",e=>{const t=timeAtEvent(e),n=trackAtEvent(e);openTimelineMenu(e,project.clips.find(e=>e.track===n&&t>=e.start&&t<clipEnd(e)))}),els.ruler.addEventListener("pointerdown",startScrub),els.playhead.addEventListener("pointerdown",startScrub),els.timelineScroll.addEventListener("dragover",e=>{if(!e.dataTransfer.types.includes("text/fablecut-media"))return;e.preventDefault();const t=getMedia(runtime.dragMediaId);if(!t)return;const n=timelineDropTarget(e,Timeline.kindOf(t));if(e.dataTransfer.dropEffect=n?"copy":"none",!n)return void clearDropPreview();const i=t.duration||5,r=snapTime(timeAtEvent(e),null);showDropPreview(n,n.newTrack?r:Timeline.firstGap(project.clips,n.trackId,r,i),i)}),els.timelineScroll.addEventListener("dragleave",e=>{els.timelineScroll.contains(e.relatedTarget)||clearDropPreview()}),window.addEventListener("dragend",()=>{runtime.dragMediaId=null,clearDropPreview()}),els.timelineScroll.addEventListener("drop",e=>{const t=e.dataTransfer.getData("text/fablecut-media");if(runtime.dragMediaId=null,!t)return void clearDropPreview();e.preventDefault();const n=getMedia(t),i=n?timelineDropTarget(e,Timeline.kindOf(n)):null;if(clearDropPreview(),!i)return;const r=snapTime(timeAtEvent(e),null);null==n.duration&&"image"!==n.kind&&"svg"!==n.kind||"svg"===n.kind&&!runtime.mediaAux.get(n.id)?.svgText?addLibraryItem({name:n.name,src:n.src},i.trackId,r,i):addClipFromMedia(n,i.trackId,r,i)});let dragDepth=0;function timelineMinZoom(){const e=projDur();return e>0?Math.min(300,.95*(els.timelineScroll.clientWidth||800)/e):1}function updateTimelineZoomControls(){const e=timelineMinZoom();state.gesture||(state.pps=clamp(state.pps,e,300)),els.zoomSlider.min=e,els.zoomSlider.step="any",els.zoomSlider.value=state.pps,$("btnZoomOut").disabled=state.pps<=e,$("btnZoomIn").disabled=state.pps>=300}function setZoom(e,t){const n=els.timelineScroll,i=n.getBoundingClientRect(),r=null!=t?t-i.left:i.width/2,a=(n.scrollLeft+r)/state.pps;state.pps=clamp(e,timelineMinZoom(),300),els.zoomSlider.value=state.pps,state.dirtyTimeline=!0,rebuildClips(),scrollTimeline(Math.max(0,a*state.pps-r))}function zoomToFit(){setZoom(.95*(els.timelineScroll.clientWidth||800)/(projDur()||1)),scrollTimeline(0)}function zoomToSelection(){const e=selectedClips();e.length?zoomToRange(Math.min(...e.map(e=>e.start)),Math.max(...e.map(e=>e.start+e.duration))):toast(uiText("Select a clip to zoom to"))}function zoomToWorkArea(){const e=project.inPoint,t=project.outPoint;null==e||null==t||t<=e?toast(uiText("Set IN and OUT markers first (I / O)")):zoomToRange(e,t)}function zoomToRange(e,t){const n=Math.max(t-e,.05),i=els.timelineScroll.clientWidth||800,r=clamp(.9*i/n,timelineMinZoom(),300);state.pps=r,els.zoomSlider.value=r,rebuildClips();const a=(e+t)/2,o=Math.max(0,contentWidth()-i);scrollTimeline(clamp(a*r-i/2,0,o))}function selectedMediaIds(){const e=new Set;for(const t of selectedClips())t.mediaId&&e.add(t.mediaId);return e}function clearBinSelectionHighlight(){if(els.binList)for(const e of els.binList.querySelectorAll(".bin-item.selected"))e.classList.remove("selected")}function syncBinSelectionFromTimeline(){if(!els.binList||!getSetting("linkSelect"))return;const e=selectedMediaIds();for(const t of els.binList.querySelectorAll(".bin-item[data-media-id]"))t.classList.toggle("selected",e.has(t.dataset.mediaId))}function selectClipsByMediaId(e){const t=project.clips.filter(t=>t.mediaId===e);if(!t.length)return setSelection([]),void toast(uiText("No clips on the timeline use this media"));t.sort((e,t)=>e.start-t.start||String(e.id).localeCompare(String(t.id)));const n=t.find(e=>"video"===e.kind)||t[0];"project"!==state.binTab&&setBinTab("project"),setSelection(t.map(e=>e.id),n.id)}function setSelection(e,t){runtime.timelineMenu&&closeTimelineMenu(),state.selIds=new Set(e),state.selId=void 0!==t?t:[...state.selIds].pop()??null,state.selId&&!state.selIds.has(state.selId)&&state.selIds.add(state.selId),state.dirtyTimeline=!0,renderInspector(),syncBinSelectionFromTimeline()}function selectClip(e,t){if(t&&t.toggle&&null!=e){const t=new Set(state.selIds);return state.transFocus=null,void(t.has(e)?(t.delete(e),setSelection([...t])):(t.add(e),setSelection([...t],e)))}const n=null==e?null:t?.transFocus??null;state.selId===e&&state.selIds.size<=1&&n===state.transFocus||(state.transFocus=n,setSelection(null==e?[]:[e],e??null))}function pruneSelection(){state.selIds=new Set([...state.selIds].filter(e=>getClip(e))),state.selIds.has(state.selId)||(state.selId=[...state.selIds].pop()??null),syncBinSelectionFromTimeline()}window.addEventListener("dragenter",e=>{e.dataTransfer?.types.includes("Files")&&(dragDepth++,document.body.classList.add("file-drag"))}),window.addEventListener("dragleave",()=>{--dragDepth<=0&&(dragDepth=0,document.body.classList.remove("file-drag"))}),window.addEventListener("dragover",e=>{e.dataTransfer?.types.includes("Files")&&e.preventDefault()}),window.addEventListener("drop",e=>{dragDepth=0,document.body.classList.remove("file-drag"),e.dataTransfer?.files.length&&(e.preventDefault(),importFiles(e.dataTransfer.files))}),els.zoomSlider.addEventListener("input",()=>setZoom(+els.zoomSlider.value)),$("btnZoomOut").addEventListener("click",()=>setZoom(state.pps/1.25)),$("btnZoomIn").addEventListener("click",()=>setZoom(1.25*state.pps)),$("btnZoomFit").addEventListener("click",zoomToFit),els.timelineScroll.addEventListener("wheel",e=>{e.preventDefault(),(e.ctrlKey||e.metaKey)&&(e.preventDefault(),setZoom(state.pps*(e.deltaY<0?1.15:1/1.15),e.clientX))},{passive:!1});const selectedClips=()=>project.clips.filter(e=>state.selIds.has(e.id));function focusTextContent(){if("text"!==getClip(state.selId)?.kind)return;const e=els.inspector.querySelector('textarea[data-k="text"]');e&&(e.focus({preventScroll:!0}),e.select(),e.scrollIntoView({block:"nearest"}))}function renderInspector(e){e&&getClip(state.selId)||closeFontPicker();const t=getClip(state.selId);if(!t)return els.inspector.innerHTML=`<div class="inspector-empty">${uiText("Select a clip to edit its")}<br>${uiText("transform, effects & audio.")}</div>`,void renderKfGraphsPanel();if(e){const e=els.inspector.querySelector("[data-k=start]"),n=els.inspector.querySelector("[data-k=duration]");return e&&(e.value=t.start.toFixed(2)),void(n&&(n.value=t.duration.toFixed(2)))}const n=t.props,i=e=>t.keyframes&&t.keyframes[e]?t.keyframes[e].length:0,r=(e,t,n="",r)=>`<div class="insp-row">${((e,t="",n)=>{e=escapeHtml(uiText(e));const r=void 0!==n?n:t,a=(Array.isArray(r)?r:String(r||"").split(",")).map(e=>e.trim()).filter(Boolean),o=a.some(e=>Object.hasOwn(DEFAULT_PROPS,e)||"transIn"===e||"transOut"===e),s=!!t&&ANIMATABLE.includes(t);return s||o?`<label class="${[s?"kf-graph-toggle":"",s&&state.kfGraphs.has(t)?"on":"",s&&i(t)?"has-kf":"",o?"insp-reset":""].filter(Boolean).join(" ")}"${(s?` data-kfgraph="${t}"`:"")+(o?` data-reset="${a.join(",")}"`:"")} title="${uiAttr(s&&o?"Click: keyframe graph · Ctrl-click: reset":s?"Show / hide keyframe graph":"Ctrl-click to reset")}">${e}</label>`:`<label>${e}</label>`})(e,n,r)}${t}${n?(e=>ANIMATABLE.includes(e)?`<span class="kf-ctl"><button class="kf-btn${i(e)?" has":""}" data-kf="${e}" title="${uiAttr("Set keyframe at playhead")}">◆${i(e)||""}</button>${i(e)?`<button class="kf-btn" data-kfclear="${e}" title="${uiAttr("Clear keyframes")}">✕</button>`:""}</span>`:"")(n):""}</div>`,a=(e,t,n,i,a,o="")=>r(e[0].toUpperCase()+e.slice(1),`<input type="range" data-k="${e}" min="${t}" max="${n}" step="${i}" value="${a}">\n <span class="val" data-val="${e}">${a}${o}</span>`,e);let o=(state.selIds.size>1?`<div class="insp-multi">${uiText("{count} clips selected — drag moves them together, Del deletes all. Fields below edit the primary (white-outlined) clip.",{count:state.selIds.size})}</div>`:"")+`<div class="insp-section"><h3>${uiText("Clip — {kind}",{kind:uiText(t.kind)})}</h3>\n ${"text"===t.kind?"":r("Name",`<input type="text" data-k="name" value="${escapeHtml(t.name)}">`)}\n ${t.mediaId?r("Source",`<button type="button" class="btn tiny style-picker-btn" data-media-open title="${uiAttr("Replace this clip's media — keeps position, trim, keyframes and effects")}">${escapeHtml((getMedia(t.mediaId)||{}).name||uiText("Missing media"))} ▾</button>`):""}\n ${r("Start (s)",`<input type="number" data-k="start" step="0.01" value="${t.start.toFixed(2)}">`)}\n ${r("Length (s)",`<input type="number" data-k="duration" step="0.01" value="${t.duration.toFixed(2)}">`)}\n </div>`;const s=(e,t,n,i)=>r(e,`<select data-k="${t}">${n.map(e=>`<option value="${e}" ${String(e)===String(i)?"selected":""}>${escapeHtml(uiText(String(e)))}</option>`).join("")}</select>`,t),l=(e,t,n)=>r(e,`<input type="checkbox" data-k="${t}" ${n?"checked":""}>`,t);if("adjust"===t.kind?o+=`<div class="insp-section"><h3>${uiText("Adjustment layer")}</h3>\n ${a("opacity",0,1,.01,n.opacity)}\n </div>`:"audio"!==t.kind&&(o+=`<div class="insp-section"><h3>${uiText("Transform")}</h3>\n ${r("Position X",`<input type="number" data-k="x" value="${n.x}">`,"x")}\n ${r("Position Y",`<input type="number" data-k="y" value="${n.y}">`,"y")}\n ${a("scale",.1,4,.01,n.scale)}\n ${a("rotation",-180,180,1,n.rotation,"°")}\n ${a("opacity",0,1,.01,n.opacity)}\n ${s("Blend","blend",BLEND_MODES,n.blend)}\n </div>`),"video"!==t.kind&&"image"!==t.kind&&"svg"!==t.kind||(o+=`<div class="insp-section"><h3>${uiText("Layout")}</h3>\n ${s("Fit","fit",["contain","cover","stretch","none"],n.fit)}\n ${r("Crop L/R %",`<input type="number" data-k="cropL" min="0" max="95" value="${n.cropL}" style="max-width:58px">\n <input type="number" data-k="cropR" min="0" max="95" value="${n.cropR}" style="max-width:58px">`,"","cropL,cropR")}\n ${r("Crop T/B %",`<input type="number" data-k="cropT" min="0" max="95" value="${n.cropT}" style="max-width:58px">\n <input type="number" data-k="cropB" min="0" max="95" value="${n.cropB}" style="max-width:58px">`,"","cropT,cropB")}\n ${a("cornerRadius",0,300,1,n.cornerRadius,"px")}\n ${l("Flip H","flipH",n.flipH)}\n ${l("Flip V","flipV",n.flipV)}\n </div>`),"video"!==t.kind&&"image"!==t.kind&&"svg"!==t.kind&&"adjust"!==t.kind||(o+=`<div class="insp-section"><h3>${uiText("Filter / Color")}</h3>\n ${s("Preset","filterPreset",Object.keys(FILTER_PRESETS),n.filterPreset)}\n ${a("brightness",0,200,1,n.brightness,"%")}\n ${a("contrast",0,200,1,n.contrast,"%")}\n ${a("saturation",0,200,1,n.saturation,"%")}\n ${a("hue",-180,180,1,n.hue,"°")}\n ${a("temperature",-100,100,1,n.temperature)}\n ${a("tint",-100,100,1,n.tint)}\n ${a("blur",0,20,.5,n.blur,"px")}\n ${a("grayscale",0,100,1,n.grayscale,"%")}\n ${a("sepia",0,100,1,n.sepia,"%")}\n ${a("invert",0,100,1,n.invert,"%")}\n ${a("vignette",0,100,1,n.vignette,"%")}\n </div>\n <div class="insp-section"><h3>${uiText("Motion FX")}</h3>\n ${a("shake",0,40,.5,n.shake,"px")}\n ${a("shakeSpeed",1,30,.5,n.shakeSpeed)}\n ${a("rgbSplit",0,30,.5,n.rgbSplit,"px")}\n ${a("grain",0,100,1,n.grain,"%")}\n </div>`),"video"!==t.kind&&"image"!==t.kind||(o+=`<div class="insp-section"><h3>${uiText("Keying / Cut-out")}</h3>\n ${r("Key color",`<input type="color" data-k="chromaKey" value="${n.chromaKey||"#00ff00"}">\n <button class="btn tiny${n.chromaKey?"":" toggle on"}" data-action="keyoff" title="${uiAttr("Disable chroma key")}">${uiText("off")}</button>`,"","chromaKey")}\n ${a("chromaTolerance",0,100,1,n.chromaTolerance)}\n ${a("chromaSoftness",0,100,1,n.chromaSoftness)}\n ${l("AI bg remove","bgRemove",n.bgRemove)}\n </div>`),"video"===t.kind||"audio"===t.kind){const e=t.props?.audioChannel,i=0===e?uiText("Left"):1===e?uiText("Right"):Number.isInteger(e)?uiText("Channel {number}",{number:e+1}):null;o+=`<div class="insp-section"><h3>${uiText("Audio / Time")}</h3>\n ${i?r("Channel",`<span style="opacity:.75">${i}</span>`):""}\n ${a("volume",0,2,.01,n.volume)}\n ${a("speed",.25,4,.05,n.speed,"×")}\n </div>`}const c=(e,t,n)=>`<div class="insp-row${state.transFocus===("transIn"===t?"in":"out")?" trans-active":""}"><label class="insp-reset" data-reset="${t}" title="${uiAttr("Ctrl-click to reset")}">${escapeHtml(uiText(e))}</label>\n <span class="insp-ctrls"><select data-k="${t}">${TRANSITIONS.map(e=>`<option value="${e}" ${e===(n?.type||"none")?"selected":""}>${uiText(e)}</option>`).join("")}</select>\n <input type="number" class="insp-dur" data-k="${t}Dur" step="0.1" min="0.1" value="${n?.duration??1}"></span></div>`;"text"===t.kind&&(o+=`<div class="insp-section"><h3>${uiText("Text")}</h3>\n ${r("Content",`<textarea data-k="text">${escapeHtml(n.text)}</textarea>`,"","text")}\n ${r(hasTextBox(n)&&n.boxFit?"Max size":"Font size",`<input type="range" data-k="fontSize" min="12" max="300" step="1" value="${n.fontSize}">\n <span class="val" data-val="fontSize">${n.fontSize}px</span>`,"fontSize")}\n ${r("Box W/H",`<span class="insp-ctrls">\n <input type="number" data-k="boxW" min="0" step="1" value="${n.boxW||0}" title="${uiAttr("Width in px (0 = no box — hug content)")}" style="max-width:64px">\n <input type="number" data-k="boxH" min="0" step="1" value="${n.boxH||0}" title="${uiAttr("Height in px (0 = no box — hug content)")}" style="max-width:64px">\n </span>`,"","boxW,boxH")}\n ${hasTextBox(n)?l("Scale to fit","boxFit",!!n.boxFit):""}\n ${r("Color",`<span class="insp-ctrls"><input type="color" data-k="color" value="${n.color}">\n <input type="color" data-k="color2" value="${n.color2||n.color}" title="${uiAttr("Gradient bottom color")}">\n <button class="btn tiny${n.color2?"":" toggle on"}" data-action="grad-off" title="${uiAttr("Disable gradient")}">${uiText("flat")}</button></span>`,"","color,color2")}\n ${s("Align","align",["left","center","right","justify"],n.align)}\n ${hasTextBox(n)?s("V-align","vAlign",["top","middle","bottom"],n.vAlign||"middle"):""}\n ${s("Direction","direction",["auto","ltr","rtl"],n.direction||"auto")}\n </div>\n <div class="insp-section"><h3>${uiText("Font")}</h3>\n ${r("Family",`<button type="button" class="btn tiny style-picker-btn" data-font-open aria-haspopup="listbox" aria-expanded="false" aria-label="${uiAttr("Family")}">${escapeHtml(n.font)} ▾</button>`,"","font")}\n ${s("Weight","weight",[0,300,400,500,600,700,800,900],n.weight)}\n ${l("Bold","bold",n.bold)}\n ${l("Italic","italic",n.italic)}\n ${l("Uppercase","uppercase",n.uppercase)}\n ${a("letterSpacing",-10,60,.5,n.letterSpacing,"px")}\n ${a("lineHeight",.7,2.5,.05,n.lineHeight)}\n </div>\n <div class="insp-section"><h3>${uiText("Text style")}</h3>\n ${a("strokeWidth",0,20,.5,n.strokeWidth,"px")}\n ${r("Stroke col.",`<input type="color" data-k="strokeColor" value="${n.strokeColor}">`,"","strokeColor")}\n ${r("Bg color",`<input type="color" data-k="bgColor" value="${n.bgColor}">`,"","bgColor")}\n ${a("bgOpacity",0,1,.05,n.bgOpacity)}\n ${a("textShadow",0,40,1,n.textShadow)}\n ${a("glow",0,100,1,n.glow)}\n ${r("Glow color",`<input type="color" data-k="glowColor" value="${n.glowColor||n.color}">\n <button class="btn tiny${n.glowColor?"":" toggle on"}" data-action="glow-auto" title="${uiAttr("Glow uses the text color")}">${uiText("auto")}</button>`,"","glowColor")}\n </div>\n <div class="insp-section"><h3>${uiText("Text")}</h3>\n ${r("Text style",`<button type="button" class="btn tiny style-picker-btn" data-style-open title="${uiAttr("Pick a style — hover to preview it live")}">${uiText((TITLE_STYLES[t.styleName]||{}).label||"Choose…")} ▾</button>\n <button class="btn tiny" data-action="title-shuffle" title="${uiAttr("Random style")}">${uiText("Shuffle")}</button>`)}\n ${r("Animation",`<select data-k="textAnim">${TEXT_ANIMS.map(e=>`<option value="${e}" ${e===n.textAnim?"selected":""}>${uiText(e)}</option>`).join("")}</select>`,"","textAnim")}\n ${a("wordRate",.05,.6,.01,n.wordRate,"s")}\n </div>`),o+=`<div class="insp-section"><h3>${uiText("Transition")}</h3>\n ${c("In","transIn",t.transitionIn)}\n ${c("Out","transOut",t.transitionOut)}\n </div>`,els.inspector.innerHTML=o,els.inspector.querySelectorAll("label.insp-reset[data-reset]").forEach(e=>{e.addEventListener("click",n=>{if(!n.ctrlKey&&!n.metaKey)return;n.preventDefault();const i=e.dataset.reset.split(",").map(e=>e.trim()).filter(Boolean);if(i.length){pushUndo();for(const e of i)"transIn"!==e&&"transOut"!==e?Object.hasOwn(DEFAULT_PROPS,e)&&(t.props[e]=DEFAULT_PROPS[e],t.keyframes?.[e]&&(delete t.keyframes[e],Object.keys(t.keyframes).length||(t.keyframes=void 0),state.dirtyTimeline=!0),"text"===e&&"text"===t.kind&&(t.name=t.props.text),"text"!==e&&"font"!==e||(state.dirtyTimeline=!0),"speed"===e&&syncLinkedSpeed(t)):(t["transIn"===e?"transitionIn":"transitionOut"]=void 0,state.dirtyTimeline=!0);scheduleSave(),renderInspector()}})}),els.inspector.querySelectorAll("[data-k]").forEach(e=>{const n=e.dataset.k;e.addEventListener("input",()=>{let i="checkbox"===e.type?e.checked:"range"===e.type||"number"===e.type?parseFloat(e.value):e.value;if("weight"===n&&(i=+i||0),"name"===n)t.name=String(i),state.dirtyTimeline=!0;else if("start"===n)t.start=Math.max(0,+i||0),syncLinkedTiming(t),state.dirtyTimeline=!0;else if("duration"===n){const e=withLinked([t]),n=trimGestureBounds(t,e);t.duration=clamp(+i||.05,.05,t.duration+n.right),syncLinkedTiming(t),state.dirtyTimeline=!0}else if("transIn"===n||"transOut"===n){const e="transIn"===n?"transitionIn":"transitionOut",r="transIn"===n?"in":"out",a=Math.max(MIN_TRANS_DUR,parseFloat(els.inspector.querySelector(`[data-k="${n}Dur"]`)?.value)||1);t[e]="none"===i?void 0:{type:String(i),duration:a},t[e]&&saveLastTransition(r,t[e]),state.dirtyTimeline=!0}else if("transInDur"===n||"transOutDur"===n){const e="transInDur"===n?"transitionIn":"transitionOut",r="transInDur"===n?"in":"out";t[e]&&(t[e].duration=Math.max(MIN_TRANS_DUR,+i||1),saveLastTransition(r,t[e]),state.dirtyTimeline=!0)}else t.props[n]=i,"text"===n&&(t.name=String(i),state.dirtyTimeline=!0);"speed"===n&&syncLinkedSpeed(t),"start"!==n&&"duration"!==n||(e.value=t[n].toFixed(2));const r=els.inspector.querySelector(`[data-val="${n}"]`);r&&(r.textContent=e.value),scheduleSave()}),e.addEventListener("focus",()=>pushUndo(),{once:!0})}),els.inspector.querySelectorAll("[data-action]").forEach(e=>{e.addEventListener("click",()=>{const n=e.dataset.action;if(pushUndo(),"keyoff"===n)t.props.chromaKey="";else if("grad-off"===n)t.props.color2="";else if("glow-auto"===n)t.props.glowColor="";else if("title-shuffle"===n){const e=Object.keys(TITLE_STYLES).filter(e=>"plain"!==e&&e!==t.styleName);applyTitleStyle(t,e[Math.floor(Math.random()*e.length)],{keepTransform:!0})}scheduleSave(),renderInspector()})});const d=els.inspector.querySelector("[data-font-open]");if(d&&(d.style.fontFamily=`${JSON.stringify(n.font)}, sans-serif`,d.addEventListener("click",()=>{runtime.fontMenu?closeFontPicker():openFontPicker(d,t)}),d.addEventListener("keydown",e=>{e.stopPropagation(),"ArrowDown"!==e.key&&"ArrowUp"!==e.key||(e.preventDefault(),runtime.fontMenu||openFontPicker(d,t))})),els.inspector.querySelectorAll("[data-style-open]").forEach(e=>{e.addEventListener("click",n=>{n.stopPropagation(),runtime.styleMenu?closeStylePicker():openStylePicker(e,t)})}),els.inspector.querySelectorAll("[data-media-open]").forEach(e=>{e.addEventListener("click",n=>{n.stopPropagation(),runtime.mediaMenu?closeMediaPicker():openMediaPicker(e,t)})}),els.inspector.querySelectorAll("[data-kf]").forEach(e=>{e.addEventListener("click",()=>{const n=e.dataset.kf,i=els.inspector.querySelector(`[data-k="${n}"]`),r=i?parseFloat(i.value):+(t.props[n]||0);if(isNaN(r))return;pushUndo(),t.keyframes||(t.keyframes={});const a=t.keyframes[n]=t.keyframes[n]||[],o=+clamp(state.time-t.start,0,t.duration).toFixed(3),s=a.find(e=>Math.abs(e.t-o)<.5/project.fps);s?s.v=r:a.push({t:o,v:r}),a.sort((e,t)=>e.t-t.t),"speed"===n&&syncLinkedSpeed(t),state.dirtyTimeline=!0,scheduleSave(),renderInspector()})}),els.inspector.querySelectorAll("[data-kfclear]").forEach(e=>{e.addEventListener("click",()=>{pushUndo(),delete t.keyframes[e.dataset.kfclear],Object.keys(t.keyframes).length||(t.keyframes=void 0),"speed"===e.dataset.kfclear&&syncLinkedSpeed(t),state.dirtyTimeline=!0,scheduleSave(),renderInspector()})}),state.transFocus){const e="in"===state.transFocus?"transIn":"transOut",t=els.inspector.querySelector(`[data-k="${e}"]`)?.closest(".insp-row");t?.scrollIntoView({block:"nearest",behavior:"smooth"})}els.inspector.querySelectorAll("[data-kfgraph]").forEach(e=>{e.addEventListener("click",t=>{t.ctrlKey||t.metaKey||(t.preventDefault(),toggleKfGraph(e.dataset.kfgraph))})}),renderKfGraphsPanel()}const KF_GRAPH_LABEL={x:"Pos X",y:"Pos Y",scale:"Scale",rotation:"Rotation",opacity:"Opacity",volume:"Volume",speed:"Speed",brightness:"Bright",contrast:"Contrast",saturation:"Sat",hue:"Hue",blur:"Blur",grayscale:"Gray",sepia:"Sepia",invert:"Invert",temperature:"Temp",tint:"Tint",vignette:"Vignette",cornerRadius:"Radius",shake:"Shake",rgbSplit:"RGB",grain:"Grain",fontSize:"Size",letterSpacing:"Track",glow:"Glow"};function toggleKfGraph(e){ANIMATABLE.includes(e)&&(state.kfGraphs.has(e)?state.kfGraphs.delete(e):state.kfGraphs.add(e),renderInspector())}function renderKfGraphsPanel(){const e=els.kfGraphs;if(!e)return;const t=getClip(state.selId),n=[...state.kfGraphs].filter(e=>ANIMATABLE.includes(e));if(!t||!n.length)return e.innerHTML="",void(e.hidden=!0);e.hidden=!1,e.innerHTML=n.map(e=>`<div class="kf-graph" data-kfgraph-card="${e}">\n <div class="kf-graph-head"><span>${uiText(KF_GRAPH_LABEL[e]||e)}</span>\n <button type="button" data-kfgraph-close="${e}" title="${uiAttr("Close graph")}">✕</button></div>\n <canvas width="160" height="52"></canvas>\n </div>`).join(""),e.querySelectorAll("[data-kfgraph-close]").forEach(e=>{e.addEventListener("click",t=>{t.stopPropagation(),state.kfGraphs.delete(e.dataset.kfgraphClose),renderInspector()})}),e.querySelectorAll(".kf-graph canvas").forEach(e=>{const t=e.closest("[data-kfgraph-card]")?.dataset.kfgraphCard;e.addEventListener("pointerdown",n=>seekKfGraph(n,e,t))}),updateKfGraphs()}function seekKfGraph(e,t,n){const i=getClip(state.selId);if(!i||!n)return;const{t0:r,t1:a}=kfGraphRange(i,n),o=t.getBoundingClientRect(),s=clamp((e.clientX-o.left)/Math.max(1,o.width),0,1);setTime(i.start+r+s*Math.max(1e-6,a-r)),updateKfGraphs()}function kfGraphRange(e,t){const n=+(e.props?.[t]??DEFAULT_PROPS[t]??0),i=Math.max(.05,e.duration),r=Array.isArray(e.keyframes?.[t])?e.keyframes[t]:[],a=r.length?r.map(e=>+e.v):[n];let o=Math.min(...a),s=Math.max(...a);if(isFinite(o)&&isFinite(s)||(o=0,s=1),Math.abs(s-o)<1e-6){const e=Math.max(.05,.1*Math.abs(o)||.5);o-=e,s+=e}else{const e=.12*(s-o);o-=e,s+=e}let l=0,c=i;if(1===r.length){const e=clamp(+r[0].t||0,0,i),t=Math.max(.15,.08*i);l=Math.max(0,e-t),c=Math.min(i,e+t)}else if(r.length>=2){const e=r.map(e=>+e.t||0),t=Math.min(...e),n=Math.max(...e),a=Math.max(.05,.1*(n-t),.02*i);l=Math.max(0,t-a),c=Math.min(i,n+a)}return c-l<.001&&(l=0,c=i),{lo:o,hi:s,fallback:n,t0:l,t1:c,dur:i}}function updateKfGraphs(){const e=els.kfGraphs;if(!e||e.hidden)return;const t=getClip(state.selId);t&&e.querySelectorAll(".kf-graph").forEach(e=>{const n=e.dataset.kfgraphCard,i=e.querySelector("canvas");n&&i&&drawKfGraph(i,t,n)})}function drawKfGraph(e,t,n){const i=window.devicePixelRatio||1,r=e.clientWidth||160,a=e.clientHeight||52;e.width===Math.round(r*i)&&e.height===Math.round(a*i)||(e.width=Math.round(r*i),e.height=Math.round(a*i));const o=e.getContext("2d");o.setTransform(i,0,0,i,0,0);const s=r,l=a;o.clearRect(0,0,s,l);const{lo:c,hi:d,fallback:u,t0:p,t1:m}=kfGraphRange(t,n),f=Math.max(1e-6,m-p),h=e=>l-4-(e-c)/(d-c)*(l-8),g=e=>3+(e-p)/f*(s-6);o.strokeStyle="#ffffff10",o.lineWidth=1,o.beginPath();const v=h(0);v>4&&v<l-4&&(o.moveTo(3,v),o.lineTo(s-3,v),o.stroke()),o.strokeStyle="#7b6cff",o.lineWidth=1.5,o.beginPath();const x=Math.max(24,Math.floor(s));for(let e=0;e<=x;e++){const i=p+e/x*f,r=kfChannel(t,n,i,u),a=g(i),s=h(r);0===e?o.moveTo(a,s):o.lineTo(a,s)}o.stroke();const b=t.keyframes?.[n];if(Array.isArray(b)){o.fillStyle="#ffd166";for(const e of b){const t=g(e.t),n=h(e.v);o.beginPath(),o.moveTo(t,n-3.5),o.lineTo(t+3.5,n),o.lineTo(t,n+3.5),o.lineTo(t-3.5,n),o.closePath(),o.fill()}}const y=state.time-t.start;if(y>=p-1e-6&&y<=m+1e-6){const e=g(clamp(y,p,m));o.strokeStyle="#ff4d6a",o.lineWidth=1,o.beginPath(),o.moveTo(e,1),o.lineTo(e,l-1),o.stroke();const i=kfChannel(t,n,clamp(y,p,m),u);o.fillStyle="#ff4d6a",o.beginPath(),o.arc(e,h(i),2.5,0,2*Math.PI),o.fill()}}function getClipEl(e){let t=runtime.clipEls.get(e.id);if(t)return t;const n=getMedia(e.mediaId);if(!n)return null;t=document.createElement("audio"===e.kind?"audio":"video"),t.preload="auto",t.src=mediaLoadSrc(n),t.playsInline=!0;const i=n.src;return t.addEventListener("error",()=>{runtime.clipEls.get(e.id)===t&&n.src===i&&setMediaFailure(i,"unreadable")}),runtime.clipEls.set(e.id,t),hookAudio(e,t),t}function releaseClipEl(e){runtime.previewMedia.delete(e);const t=runtime.clipEls.get(e);if(t){try{t.pause(),t.removeAttribute("src"),t.load()}catch{}runtime.clipEls.delete(e)}const n=runtime.clipGain.get(e);if(n){try{n.disconnect()}catch{}if(n._fcSource)try{n._fcSource.disconnect()}catch{}if(n._fcOut)try{n._fcOut.disconnect()}catch{}if(n._fcSplit)try{n._fcSplit.disconnect()}catch{}runtime.clipGain.delete(e)}}function stealExportVideo(e,t){if(runtime.clipEls.get(e.id))return runtime.clipEls.get(e.id);let n=null,i=null;for(const[r,a]of runtime.clipEls){const o=getClip(r);o&&"video"===o.kind&&o.mediaId===e.mediaId&&!activeAt(o,t)&&(!i||a.readyState>=1&&i.readyState<1)&&(n=r,i=a)}if(!i)return null;runtime.clipEls.delete(n),runtime.previewMedia.delete(n);const r=runtime.clipGain.get(n);return r&&(runtime.clipGain.delete(n),runtime.clipGain.set(e.id,r)),runtime.clipEls.set(e.id,i),i}function acquireExportVideo(e,t){return runtime.clipEls.get(e.id)||stealExportVideo(e,t)||getClipEl(e)}function releaseFinishedExportVideos(e){for(const t of[...runtime.clipEls.keys()]){const n=getClip(t);"video"!==n?.kind||activeAt(n,e)||releaseClipEl(t)}}function ensureAudio(){if(runtime.audio)return runtime.audio;const e=new(window.AudioContext||window.webkitAudioContext),t=e.createGain(),n=e.createGain(),i=e.createMediaStreamDestination(),r=visibleTracks().filter(e=>"audio"===e.kind),a={};for(const t of r)a[t.id]=e.createGain();n.connect(e.destination),t.connect(n),t.connect(i);for(const e of Object.keys(a))a[e].connect(t);runtime.audio={ctx:e,master:t,monitor:n,recDest:i,trackBus:a,audioTrackIds:r.map(e=>e.id),meters:[],meterReady:!1},installMeterWorklet(runtime.audio).catch(()=>{});for(const[e,t]of runtime.clipEls){const n=getClip(e);n&&hookAudio(n,t)}return runtime.audio}function connectChannelIsolated(e,t,n,i){if(0===i||1===i){const r=e.createChannelSplitter(2),a=e.createChannelMerger(2);return t.connect(r),r.connect(n,i),n.connect(a,0,i),{out:a,split:r,merge:a}}return t.connect(n),{out:n,split:null,merge:null}}function hookAudio(e,t){if(runtime.audio&&!runtime.clipGain.has(e.id)&&("video"===e.kind||"audio"===e.kind))try{const n=runtime.audio.ctx,i=n.createMediaElementSource(t),r=n.createGain();r._fcSource=i,r.gain.value=0;const a=e.props?.audioChannel,{split:o,merge:s}=connectChannelIsolated(n,i,r,a);o&&(r._fcSplit=o,r._fcOut=s,r._fcChannel=a),runtime.clipGain.set(e.id,r),routeClipGain(e),t.volume=1}catch{}}function routeClipGain(e){const t=runtime.clipGain.get(e.id);if(!t||!runtime.audio)return;const n=runtime.audio.trackBus[e.track]||runtime.audio.master,i=t._fcOut||t;if(i._fcBus!==n){try{i.disconnect()}catch{}i.connect(n),i._fcBus=n}}const METER_SEGS=16,METER_DB_MIN=-48,METER_DB_MAX=0,METER_DB_MARKS=[0,-6,-12,-24,-36,-48],METER_MODES=["rms","lufs","peak"],METER_MODE_LABEL={rms:"RMS",lufs:"LUFS",peak:"PEAK"},METER_SEG_W=8,METER_SEG_H=12,METER_SEG_GAP=2,METER_COL_W=8,METER_COL_H=222;function makeMeterCanvas(e){const t=window.devicePixelRatio||1;e.style.width="8px",e.style.height="222px",e.width=Math.round(8*t),e.height=Math.round(222*t);const n=e.getContext("2d");return n.setTransform(t,0,0,t,0,0),{canvas:e,ctx:n}}function paintMeterSegs(e,t,n){const i=e.ctx;i.clearRect(0,0,8,222);for(let e=0;e<16;e++){const r=222-12*(e+1)-2*e,a=e<t||e===n,o=e/15;i.beginPath(),i.roundRect(0,r,8,12,1),a?(i.fillStyle=o<.6?"#3dd68c":o<.85?"#f0c14a":"#e5484d",i.fill()):(i.fillStyle="#2a2a33",i.fill(),i.strokeStyle="#0006",i.lineWidth=1,i.stroke())}}const meterState={mode:(()=>{try{const e=localStorage.getItem("fablecut-meter-mode");return METER_MODES.includes(e)?e:"rms"}catch{return"rms"}})(),trackIds:[],rms:{},peak:{},lufs:{},disp:{},peakHold:{},peakHoldT:{},segs:{},lastLit:{},lastHold:{},modeBtn:null};function audioMeterTracks(){return visibleTracks().filter(e=>"audio"===e.kind)}function cycleMeterMode(e){e&&(e.preventDefault(),e.stopPropagation());const t=METER_MODES.indexOf(meterState.mode);meterState.mode=METER_MODES[(t+1)%METER_MODES.length];try{localStorage.setItem("fablecut-meter-mode",meterState.mode)}catch{}meterState.modeBtn&&(meterState.modeBtn.textContent=METER_MODE_LABEL[meterState.mode]);const n=$("vuMeter");n&&(n.title=uiText("Mode: {mode} — click to switch",{mode:METER_MODE_LABEL[meterState.mode]}));for(const e of meterState.trackIds)meterState.disp[e]=METER_DB_MIN,meterState.peakHold[e]=METER_DB_MIN,meterState.peakHoldT[e]=0}async function installMeterWorklet(e){if(e.meterReady||!e.ctx.audioWorklet||e._meterLoading||!e.audioTrackIds.length)return;e._meterLoading=!0;const t=e.meterVersion||0,n=[];try{if(await e.ctx.audioWorklet.addModule("meter-worklet.js?v=2"),t!==(e.meterVersion||0))return;const i=e.audioTrackIds.slice();for(let t=0;t<i.length;t+=16){const r=i.slice(t,t+16),a=new AudioWorkletNode(e.ctx,"fablecut-meter",{numberOfInputs:r.length,numberOfOutputs:1,outputChannelCount:[2],channelCount:2,channelCountMode:"explicit",processorOptions:{hopBlocks:8,nTracks:r.length,trackIds:r}});n.push(a),a.port.onmessage=e=>{const t=e.data;if(t&&"meter"===t.type)for(let e=0;e<r.length;e++)meterState.rms[r[e]]=t.rms[e]||0,meterState.peak[r[e]]=t.peak[e]||0,meterState.lufs[r[e]]=t.lufs[e]??-70}}for(let t=0;t<i.length;t++){const r=e.trackBus[i[t]];r.disconnect(),r.connect(n[Math.floor(t/16)],0,t%16)}for(const t of n)t.connect(e.monitor),t.connect(e.recDest);e.meters=n,e.meterReady=!0;for(const e of i)meterState.rms[e]=0,meterState.peak[e]=0,meterState.lufs[e]=-70,meterState.disp[e]=METER_DB_MIN,meterState.peakHold[e]=METER_DB_MIN,meterState.peakHoldT[e]=0;buildMeterDOM()}catch(t){for(const e of n)e.port.onmessage=null,e.disconnect();for(const t of Object.values(e.trackBus))t.disconnect(),t.connect(e.master);console.warn("[FableCut] meter worklet unavailable:",t)}finally{e._meterLoading=!1,t!==(e.meterVersion||0)&&installMeterWorklet(e).catch(()=>{})}}function buildMeterDOM(){const e=$("vuMeter");if(!e)return;const t=audioMeterTracks();e.innerHTML="",e.title=uiText("Mode: {mode} — click to switch",{mode:METER_MODE_LABEL[meterState.mode]});const n=document.createElement("button");n.type="button",n.className="vu-mode",n.textContent=METER_MODE_LABEL[meterState.mode],n.title=uiText("Cycle RMS → LUFS → Peak"),n.addEventListener("click",cycleMeterMode),e.appendChild(n),meterState.modeBtn=n;const i=document.createElement("div");i.className="vu-channels";const r=document.createElement("div");r.className="vu-scale";const a=METER_DB_MAX-METER_DB_MIN;for(const e of METER_DB_MARKS){const t=document.createElement("span");t.className="vu-scale-tick",t.textContent=0===e?"0":String(e),t.style.top=(METER_DB_MAX-e)/a*100+"%",r.appendChild(t)}i.appendChild(r),meterState.segs={},meterState.lastLit={},meterState.lastHold={},meterState.trackIds=t.map(e=>e.id);for(const e of t){null==meterState.disp[e.id]&&(meterState.disp[e.id]=METER_DB_MIN,meterState.peakHold[e.id]=METER_DB_MIN,meterState.peakHoldT[e.id]=0,meterState.rms[e.id]=0,meterState.peak[e.id]=0,meterState.lufs[e.id]=-70);const t=document.createElement("div");t.className="vu-channel",t.dataset.track=e.id;const n=document.createElement("canvas");n.className="vu-segs";const r=makeMeterCanvas(n);meterState.segs[e.id]=r,paintMeterSegs(r,0,-1);const a=document.createElement("span");a.className="vu-label",a.textContent=e.id,t.appendChild(n),t.appendChild(a),i.appendChild(t)}e.appendChild(i)}function rmsToDb(e){return e>1e-8?20*Math.log10(e):METER_DB_MIN}function meterReadingDb(e){const t=meterState.mode;if("peak"===t)return rmsToDb(meterState.peak[e]||0);if("lufs"===t){const t=meterState.lufs[e];return null==t||t<METER_DB_MIN?METER_DB_MIN:Math.min(METER_DB_MAX,t)}return rmsToDb(meterState.rms[e]||0)}function updateMeterUI(e){const t=meterState.trackIds;if(!t.length)return;const n=(state.playing||state.audioHold)&&runtime.audio?.meterReady,i=meterState.mode,r="peak"===i?.005:"lufs"===i?.04:.015,a="peak"===i?.35:"lufs"===i?.12:.18,o=1-Math.exp(-e/r),s=1-Math.exp(-e/a),l=performance.now();for(const e of t){const t=n?meterReadingDb(e):METER_DB_MIN,i=meterState.disp[e]??METER_DB_MIN,r=i+(t-i)*(t>i?o:s);meterState.disp[e]=r;const a=t;a>=(meterState.peakHold[e]??METER_DB_MIN)?(meterState.peakHold[e]=a,meterState.peakHoldT[e]=l):l-(meterState.peakHoldT[e]||0)>800&&(meterState.peakHold[e]+=(METER_DB_MIN-meterState.peakHold[e])*s);const c=meterState.segs[e];if(!c)continue;const d=(r-METER_DB_MIN)/(METER_DB_MAX-METER_DB_MIN),u=Math.round(16*clamp(d,0,1)),p=Math.round(15*clamp(((meterState.peakHold[e]??METER_DB_MIN)-METER_DB_MIN)/(METER_DB_MAX-METER_DB_MIN),0,1));meterState.lastLit[e]===u&&meterState.lastHold[e]===p||(meterState.lastLit[e]=u,meterState.lastHold[e]=p,paintMeterSegs(c,u,p))}}let previewIntersecting=!0,previewPageActive=!0;function previewUnavailable(){return document.hidden||!previewPageActive||!previewIntersecting}function stopHiddenPreview(){previewUnavailable()&&(state.exporting||state.rendering||(pause(),silencePreviewMedia()),runtime.sfxPreview&&runtime.sfxPreview.pause(),els.libList.querySelectorAll(".lib-play").forEach(e=>{e.textContent="▶"}))}function play(){if(!previewUnavailable()&&(state.audioHold&&setAudioHold(!1),!state.playing)){if(ensureAudio(),runtime.audio.ctx.resume(),playLimited()){const{start:e,end:t}=playRange();state.time>=t-.01&&state.time<=t+.02&&(state.time=e)}else state.time>=projDur()-.01&&(state.time=0);resetPreviewWait(!0),lastTs=null,state.playing=!0,runtime.followPlayhead=!0,runtime.timelineScrollLeft=els.timelineScroll.scrollLeft,els.btnPlay.textContent="⏸",els.btnPlay.classList.add("on")}}function pause(){resetPreviewWait(),state.audioHold&&setAudioHold(!1),state.playing=!1,els.btnPlay.textContent="▶",els.btnPlay.classList.remove("on");for(const e of runtime.clipEls.values())e.paused||e.pause();state.exporting&&!backgroundExportActive&&finishExport(!1)}document.addEventListener("visibilitychange",stopHiddenPreview),window.addEventListener("pagehide",()=>{previewPageActive=!1,stopHiddenPreview()}),window.addEventListener("pageshow",()=>{previewPageActive=!0,stopHiddenPreview()}),window.parent!==window&&"undefined"!=typeof IntersectionObserver&&new IntersectionObserver(e=>{const t=e[e.length-1];t&&(previewIntersecting=t.isIntersecting&&t.intersectionRect.width>0&&t.intersectionRect.height>0,stopHiddenPreview())}).observe(document.documentElement);let audioHoldGen=0,audioHoldNodes=[],audioHoldRaf=0;function disposeAudioHoldNode(e){if(e){try{e.src.stop()}catch{}try{e.src.disconnect()}catch{}try{e.src&&(e.src.buffer=null)}catch{}try{e.gain.disconnect()}catch{}if(e.split)try{e.split.disconnect()}catch{}if(e.merge)try{e.merge.disconnect()}catch{}}}function stopAudioHoldNodes(){audioHoldGen++,audioHoldRaf&&(cancelAnimationFrame(audioHoldRaf),audioHoldRaf=0);for(const e of audioHoldNodes)disposeAudioHoldNode(e);audioHoldNodes=[]}function scheduleAudioHoldRefresh(){state.audioHold&&!state.playing&&(audioHoldRaf||(audioHoldRaf=requestAnimationFrame(()=>{audioHoldRaf=0,refreshAudioHold()})))}function sliceAudioFrame(e,t,n,i){const r=t.sampleRate,a=clamp(Math.floor(n*r),0,Math.max(0,t.length-1)),o=Math.max(1,Math.min(t.length-a,Math.round(i*r))),s=e.createBuffer(t.numberOfChannels,o,r);for(let e=0;e<t.numberOfChannels;e++)s.getChannelData(e).set(t.getChannelData(e).subarray(a,a+o));return s}function refreshAudioHold(){if(!state.audioHold||state.playing||state.exporting||state.rendering)return void stopAudioHoldNodes();const e=ensureAudio();try{e.ctx.resume()}catch{}const t=state.time,n=1/Math.max(1,project.fps||30),i=++audioHoldGen;for(const e of audioHoldNodes)disposeAudioHoldNode(e);audioHoldNodes=[];for(const e of runtime.clipEls.values())e.paused||e.pause();for(const e of runtime.clipGain.values())e.gain.value=0;for(const r of project.clips){if("audio"!==r.kind&&"video"!==r.kind)continue;if(!isTrackEnabled(r.track)||!activeAt(r,t))continue;const a=getMedia(r.mediaId);if(!a||"audio"!==a.kind&&"video"!==a.kind)continue;const o=evalProps(r,t),s=clamp(+o.volume||0,0,4);s<=1e-4||getAudioBuffer(a).then(a=>{if(i!==audioHoldGen||!state.audioHold||state.playing)return;const o=mediaTimeAt(r,t);if(!(a.duration>0)||o>=a.duration)return;const l=sliceAudioFrame(e.ctx,a,o,n),c=e.ctx.createBufferSource();c.buffer=l,c.loop=!0;const d=e.ctx.createGain();d.gain.value=s;const u=r.props?.audioChannel,{out:p,split:m,merge:f}=connectChannelIsolated(e.ctx,c,d,u),h=e.trackBus[r.track]||e.master;p.connect(h);const g={src:c,gain:d,split:m,merge:f};try{c.start(0)}catch{return void disposeAudioHoldNode(g)}i===audioHoldGen&&state.audioHold&&!state.playing?audioHoldNodes.push(g):disposeAudioHoldNode(g)}).catch(()=>{})}}function setAudioHold(e){if(e=!!e){if(state.exporting||state.rendering||previewUnavailable())return;if(resetPreviewWait(),state.playing){state.playing=!1,els.btnPlay.textContent="▶",els.btnPlay.classList.remove("on");for(const e of runtime.clipEls.values())e.paused||e.pause()}state.audioHold=!0,els.btnAudioHold&&els.btnAudioHold.classList.add("on"),refreshAudioHold()}else state.audioHold=!1,els.btnAudioHold&&els.btnAudioHold.classList.remove("on"),stopAudioHoldNodes()}const PREVIEW_RATES=[1,1.5,2,4];function playRate(){return state.exporting?1:state.previewRate}function setPreviewRate(e){state.previewRate=e,els.btnSpeed.textContent=e+"×",els.btnSpeed.classList.toggle("on",1!==e)}function cyclePreviewRate(e){const t=Math.max(0,PREVIEW_RATES.indexOf(state.previewRate));setPreviewRate(PREVIEW_RATES[(t+e+PREVIEW_RATES.length)%PREVIEW_RATES.length])}function stepPreviewRate(e){const t=Math.max(0,PREVIEW_RATES.indexOf(state.previewRate));setPreviewRate(PREVIEW_RATES[clamp(t+e,0,PREVIEW_RATES.length-1)])}function activeAt(e,t){return t>=e.start&&t<clipEnd(e)}function resetPreviewWait(e=!1){if(e&&(runtime.previewRemainder=0),e)for(const e of runtime.previewMedia.values())e.error=null;runtime.previewWait&&(els.btnPlay.textContent=state.playing?"⏸":"▶"),runtime.previewWait=null,els.btnPlay.title=uiText("Play / Pause (Space)")}function silencePreviewMedia(){for(const[e,t]of runtime.clipEls){t.paused||t.pause();const n=runtime.clipGain.get(e);n?n.gain.value=0:t.volume=0}}function previewNextTime(e,t){const n=state.time;if(!state.playing||runtime.scrub)return runtime.previewRemainder=0,n;if(state.exporting)return Math.min(t,n+e*playRate());if(runtime.audio&&"running"!==runtime.audio.ctx.state)return n;for(const e of project.clips){if("video"!==e.kind&&"audio"!==e.kind||!isTrackEnabled(e.track)||!activeAt(e,n))continue;const t=runtime.clipEls.get(e.id),i=runtime.previewMedia.get(e.id);if(!t||!i?.ready||i.playPending||t.paused&&!t.ended)return n}const i=Math.min(t,n+e*playRate()+(runtime.previewRemainder||0));let r=null;for(const e of project.clips)("video"===e.kind||"audio"===e.kind)&&isTrackEnabled(e.track)&&e.start>n&&e.start<=i&&(r=Math.max(r??n,e.start));const a=r??i;return runtime.previewRemainder=i-a,a}function previewFrameTime(e=state.time,t=projDur()){if(state.exporting&&!backgroundExportActive||state.rendering||t<=0||e<t)return e;const n=Math.max(1,project.fps||30);return(Math.max(1,Math.round(t*n))-1)/n}function preparePreviewMedia(e,t=projDur()){const n=previewFrameTime(e,t),i=n!==e;e=n;const r=new Set,a=[],o=2*playRate();for(const t of project.clips){if("video"!==t.kind&&"audio"!==t.kind||!isTrackEnabled(t.track))continue;const n=activeAt(t,e);if(!(n||t.start>e&&t.start<=e+o))continue;if(clipMediaFailure(t)){n&&a.push({c:t,error:mediaFailureLabel(clipMediaFailure(t))});continue}r.add(t.id);const s=runtime.clipEls.get(t.id);s&&s.getAttribute("src")!==mediaLoadSrc(getMedia(t.mediaId))&&releaseClipEl(t.id);const l=getClipEl(t);if(!l){n&&a.push({c:t,error:"missing media"});continue}const c=JSON.stringify([getMedia(t.mediaId)?.src,t.start,t.in,t.duration,t.props?.speed,t.keyframes?.speed]);let d=runtime.previewMedia.get(t.id);d&&d.key===c||(d={key:c,target:null,ready:!1,playPending:!1,error:null},runtime.previewMedia.set(t.id,d));const u=mediaTimeAt(t,n?e:t.start),p=clamp(+evalProps(t,n?e:t.start).speed||1,.1,8),m=clamp(p*playRate(),.0625,16);if(l.playbackRate!==m)try{l.playbackRate=m}catch{}if(!n){l.paused||l.pause();const e=runtime.clipGain.get(t.id);e?e.gain.value=0:l.volume=0}l.error&&(d.error=l.error.message||"media error "+l.error.code),!("video"===t.kind&&l.readyState>=1)||l.videoWidth&&l.videoHeight||(d.error="no decodable video track");const f=i?1e-4:state.playing&&!runtime.scrub&&!l.paused&&d.ready?.25*m:.04;if(l.readyState>=1&&!l.seeking&&(null==d.target||Math.abs(l.currentTime-u)>f)){d.target=u,d.ready=!1;try{Math.abs(l.currentTime-u)>1e-4&&(l.currentTime=u)}catch(e){d.error=e.message}}const h=!state.playing||runtime.scrub||i?2:3;d.ready=null!=d.target&&!l.seeking&&l.readyState>=h&&Math.abs(l.currentTime-u)<=f&&!d.error,n&&!d.ready&&a.push({c:t,error:d.error})}for(const e of[...runtime.clipEls.keys()])r.has(e)||releaseClipEl(e);return a}function syncMedia(){const e=state.time;for(const[t,n]of runtime.clipEls){const i=getClip(t),r=runtime.previewMedia.get(t);if(i&&state.playing&&!runtime.scrub&&(!runtime.audio||"running"===runtime.audio.ctx.state)&&isTrackEnabled(i.track)&&activeAt(i,e)&&r?.ready){const a=evalProps(i,e),o=clamp(a.volume,0,4),s=runtime.clipGain.get(t);s?s.gain.value=o:n.volume=clamp(o,0,1),n.paused&&!r.playPending&&(r.playPending=!0,n.play().catch(e=>{"AbortError"!==e.name&&runtime.previewMedia.get(t)===r&&(r.error=e.message||"playback failed")}).finally(()=>{r.playPending=!1}))}else{n.paused||n.pause();const e=runtime.clipGain.get(t);e?e.gain.value=0:n.volume=0}}}function seekMediaWhilePaused(){if(state.playing&&!runtime.scrub)return;const e=previewFrameTime();for(const t of project.clips){if("video"!==t.kind)continue;if(!isTrackEnabled(t.track))continue;if(!activeAt(t,e))continue;const n=getClipEl(t);if(!n)continue;const i=mediaTimeAt(t,e);if(Math.abs(n.currentTime-i)>(e!==state.time?1e-4:.04))try{n.currentTime=i}catch{}}}const EASE={linear:e=>e,"ease-in":e=>e*e,"ease-out":e=>1-(1-e)*(1-e),"ease-in-out":e=>e<.5?2*e*e:1-Math.pow(-2*e+2,2)/2};function evalProps(e,t){const n={...e.props},i=t-e.start;if(e.keyframes)for(const[t,r]of Object.entries(e.keyframes)){if(!Array.isArray(r)||!r.length)continue;let e;if(i<=r[0].t)e=r[0].v;else if(i>=r[r.length-1].t)e=r[r.length-1].v;else for(let t=0;t<r.length-1;t++){const n=r[t],a=r[t+1];if(i>=n.t&&i<=a.t){const t=(i-n.t)/Math.max(1e-6,a.t-n.t),r=EASE[a.ease||"ease-in-out"]||EASE.linear;e=n.v+(a.v-n.v)*r(t);break}}"number"!=typeof e||isNaN(e)||(n[t]=e)}applyFilterPreset(n);const r=els.preview.width,a=els.preview.height,o=e.transitionIn,s=e.transitionOut;return o&&o.duration>0&&i<o.duration&&applyTransition(n,o.type,1-EASE["ease-out"](clamp(i/o.duration,0,1)),r,a,-1),s&&s.duration>0&&i>e.duration-s.duration&&applyTransition(n,s.type,EASE["ease-in"](clamp((i-(e.duration-s.duration))/s.duration,0,1)),r,a,1),n}function applyFilterPreset(e){const t=FILTER_PRESETS[e.filterPreset];if(t&&"none"!==e.filterPreset)for(const[n,i]of Object.entries(t))e[n]="brightness"===n||"contrast"===n||"saturation"===n?(+e[n]||100)*i/100:"grayscale"===n||"sepia"===n||"vignette"===n||"invert"===n?clamp((+e[n]||0)+i,0,100):(+e[n]||0)+i}function applyTransition(e,t,n,i,r,a){if(n>0)switch(t){case"fade":case"dissolve":e.opacity*=1-n,e.volume*=1-n;break;case"slide-left":e.x=(+e.x||0)-a*n*i;break;case"slide-right":e.x=(+e.x||0)+a*n*i;break;case"slide-up":e.y=(+e.y||0)-a*n*r;break;case"slide-down":e.y=(+e.y||0)+a*n*r;break;case"zoom":e.scale=(+e.scale||1)*(1-.6*n),e.opacity*=1-n;break;case"wipe":case"wipe-left":e._wipe=n,e._wipeDir="left";break;case"wipe-right":e._wipe=n,e._wipeDir="right";break;case"wipe-up":e._wipe=n,e._wipeDir="up";break;case"wipe-down":e._wipe=n,e._wipeDir="down";break;case"iris":e._iris=n;break;case"spin":e.rotation=(+e.rotation||0)+a*n*200,e.scale=(+e.scale||1)*(1-.4*n),e.opacity*=1-n;break;case"blur":e.blur=(+e.blur||0)+24*n,e.opacity*=1-n*n,e.volume*=1-n;break;case"whip":e.x=(+e.x||0)-a*EASE["ease-in"](n)*i*1.4,e.blur=(+e.blur||0)+16*n;break;case"glitch":{const t=Math.sin(61.7*n)*Math.sin(23.3*n);e.rgbSplit=(+e.rgbSplit||0)+14*n,e.x=(+e.x||0)+t*n*i*.06,e.opacity*=1-n*n;break}case"pop":e.scale=(+e.scale||1)*Math.max(.001,backOut(1-n)),e.opacity*=Math.min(1,2.5*(1-n))}}function shiftKF(e,t,n){if(!e)return;const i={};for(const[r,a]of Object.entries(e)){const e=a.map(e=>({...e,t:+(e.t-t).toFixed(4)})).filter(e=>e.t>=-.001&&e.t<=n+.001);e.length&&(i[r]=e)}return Object.keys(i).length?i:void 0}function parseSvgSize(e){const t=e=>{const t=parseFloat(e);return isFinite(t)&&t>0?t:0},n=t=>(e.match(new RegExp(`<svg[^>]*\\s${t}="([^"%]+)"`,"i"))||[])[1];let i=t(n("width")),r=t(n("height"));if(!i||!r){const n=(e.match(/<svg[^>]*\sviewBox="([^"]+)"/i)||[])[1];if(n){const e=n.trim().split(/[\s,]+/);i=t(e[2]),r=t(e[3])}}return{width:i||800,height:r||600}}async function loadSvgMedia(e){const t=await fetch(e.src,{signal:AbortSignal.timeout(1e4)});if(!t.ok)throw new Error("SVG load failed: "+t.status);const n=await t.text(),{width:i,height:r}=parseSvgSize(n);e.width=i,e.height=r;const a=runtime.mediaAux.get(e.id)||{};a.svgText=n,a.svgAnimated=/@keyframes|animation\s*:/i.test(n),a.svgFrames=new Map,a.svgPending=null,runtime.mediaAux.set(e.id,a),a.svgAnimated||(a.img=await loadImage(e.src)),state.dirtyTimeline=!0}function svgUrlAt(e,t){const n=`<style>*{animation-play-state:paused!important;animation-delay:calc(var(--d,0s) - ${t.toFixed(4)}s)!important}</style>`,i=e.svgText.replace(/(<svg[^>]*>)/i,`$1${n}`);return"data:image/svg+xml;charset=utf-8,"+encodeURIComponent(i)}function renderSvgFrame(e,t){return new Promise((n,i)=>{const r=new Image;r.onload=()=>n(r),r.onerror=()=>i(new Error("svg raster failed")),r.src=svgUrlAt(e,t)})}function getSvgImage(e,t){const n=runtime.mediaAux.get(e.mediaId);if(!n||!n.svgText)return null;if(!n.svgAnimated)return n.img||null;const i=Math.max(0,mediaTimeAt(e,t)),r=Math.round(i*project.fps)/project.fps;return n.svgFrames.get(r)||(n.svgPending||(n.svgPending=renderSvgFrame(n,r).then(e=>{n.svgFrames.set(r,e),n.svgFrames.size>90&&n.svgFrames.delete(n.svgFrames.keys().next().value),n.lastImg=e}).catch(()=>{}).finally(()=>{n.svgPending=null})),n.lastImg||null)}async function prepareSvgFrame(e,t){const n=runtime.mediaAux.get(e.mediaId);if(!n)return;if(!n.svgText)try{await loadSvgMedia(getMedia(e.mediaId))}catch{return}if(!n.svgAnimated)return;const i=Math.max(0,mediaTimeAt(e,t)),r=Math.round(i*project.fps)/project.fps;if(!n.svgFrames.get(r))try{const e=await renderSvgFrame(n,r);n.svgFrames.set(r,e),n.svgFrames.size>90&&n.svgFrames.delete(n.svgFrames.keys().next().value),n.lastImg=e}catch{}}const bgSeg={seg:null,loading:null,failed:!1,queue:Promise.resolve(),masks:new Map};function loadScript(e){return new Promise((t,n)=>{const i=document.createElement("script");i.src=e,i.onload=t,i.onerror=()=>n(new Error("script load failed: "+e)),document.head.appendChild(i)})}function ensureBgSeg(){if(bgSeg.seg||bgSeg.failed)return bgSeg.loading||Promise.resolve();if(!bgSeg.loading){const e="https://cdn.jsdelivr.net/npm/@mediapipe/selfie_segmentation";bgSeg.loading=loadScript(`${e}/selfie_segmentation.js`).then(()=>{const t=new SelfieSegmentation({locateFile:t=>`${e}/${t}`});t.setOptions({modelSelection:1}),t.onResults(e=>{if(!bgSeg.currentClip)return;let t=bgSeg.masks.get(bgSeg.currentClip);t||(t=document.createElement("canvas"),bgSeg.masks.set(bgSeg.currentClip,t)),t.width=e.segmentationMask.width,t.height=e.segmentationMask.height,t.getContext("2d").drawImage(e.segmentationMask,0,0)}),bgSeg.seg=t}).catch(()=>{bgSeg.failed=!0,toast(uiText("Background removal unavailable — couldn't load MediaPipe (offline?). Using chroma key still works."))})}return bgSeg.loading}function requestMask(e,t,n=!1){return ensureBgSeg(),bgSeg.seg?!n&&bgSeg.pending>0?Promise.resolve():(bgSeg.pending++,bgSeg.queue=bgSeg.queue.then(async()=>{if(0!==(t.videoWidth||t.naturalWidth||t.width||0)){bgSeg.currentClip=e;try{await bgSeg.seg.send({image:t})}catch{}}}).finally(()=>{bgSeg.pending--}),bgSeg.queue):bgSeg.loading||Promise.resolve()}bgSeg.pending=0;const scratch=document.createElement("canvas"),scratchCtx=scratch.getContext("2d",{willReadFrequently:!0});let grainTile=null;function getGrainTile(){if(grainTile)return grainTile;grainTile=document.createElement("canvas"),grainTile.width=grainTile.height=256;const e=grainTile.getContext("2d"),t=e.createImageData(256,256);for(let e=0;e<t.data.length;e+=4){const n=90+130*Math.random();t.data[e]=t.data[e+1]=t.data[e+2]=n,t.data[e+3]=255}return e.putImageData(t,0,0),grainTile}function drawGrain(e,t,n,i,r,a){const o=ctx2d.globalAlpha,s=ctx2d.globalCompositeOperation,l=7919*Math.floor(a*project.fps)%256;ctx2d.globalCompositeOperation="overlay",ctx2d.globalAlpha=o*clamp(e/100,0,1)*.55,ctx2d.save(),ctx2d.translate(-l,l),ctx2d.fillStyle=ctx2d.createPattern(getGrainTile(),"repeat"),ctx2d.fillRect(t+l,n-l,i,r),ctx2d.restore(),ctx2d.globalAlpha=o,ctx2d.globalCompositeOperation=s}const adjScratch=document.createElement("canvas");function drawAdjust(e,t,n,i){const r=evalProps(e,i);adjScratch.width!==t&&(adjScratch.width=t),adjScratch.height!==n&&(adjScratch.height=n);const a=adjScratch.getContext("2d");if(a.clearRect(0,0,t,n),a.drawImage(els.preview,0,0),ctx2d.save(),ctx2d.setTransform(1,0,0,1,0,0),r.shake>0){const e=+r.shake,t=i*clamp(+r.shakeSpeed||8,.5,40)*Math.PI*2;ctx2d.translate(Math.sin(1.3*t)*e*.6+Math.sin(2.71*t)*e*.4,Math.cos(1.7*t)*e*.5+Math.sin(3.13*t)*e*.35)}ctx2d.globalAlpha=clamp(r.opacity,0,1),ctx2d.filter=buildFilter(r);let o=adjScratch;if((r.temperature||r.tint||r.rgbSplit>0)&&(o=pixelPass(e,{...r,chromaKey:"",bgRemove:!1},adjScratch,0,0,t,n,t,n)),ctx2d.drawImage(o,0,0,o.width,o.height,0,0,t,n),ctx2d.filter="none",r.vignette>0){const e=ctx2d.createRadialGradient(t/2,n/2,.35*Math.min(t,n),t/2,n/2,.55*Math.hypot(t,n));e.addColorStop(0,"rgba(0,0,0,0)"),e.addColorStop(1,`rgba(0,0,0,${.9*clamp(r.vignette/100,0,1)})`),ctx2d.fillStyle=e,ctx2d.fillRect(0,0,t,n)}r.grain>0&&drawGrain(r.grain,0,0,t,n,i),ctx2d.restore()}function needsPixelPass(e,t){return!!(e.chromaKey||e.temperature||e.tint||e.rgbSplit>0||e.bgRemove&&bgSeg.masks.get(t.id))}function hexToRgb(e){const t=parseInt(String(e).replace("#",""),16)||0;return[t>>16&255,t>>8&255,255&t]}const pixGL={tried:!1,gl:null,canvas:null,loc:null,stage:null,stageCtx:null,outs:new Map},PIX_VERT="#version 300 es\nin vec2 aPos;\nvoid main() { gl_Position = vec4(aPos, 0.0, 1.0); }",PIX_FRAG="#version 300 es\nprecision highp float;\nuniform sampler2D uTex;\nuniform ivec2 uSize;\nuniform int uSplit;\nuniform int uKey;\nuniform vec2 uKeyC;\nuniform vec2 uKeyT;\nuniform vec2 uShift;\nout vec4 o;\nvoid main() {\n ivec2 sz = uSize;\n ivec2 q = ivec2(int(gl_FragCoord.x), sz.y - 1 - int(gl_FragCoord.y));\n vec4 c = texelFetch(uTex, q, 0);\n vec3 rgb = c.rgb * 255.0;\n float a = c.a;\n if (uSplit > 0) {\n rgb.r = texelFetch(uTex, ivec2(min(sz.x - 1, q.x + uSplit), q.y), 0).r * 255.0;\n rgb.b = texelFetch(uTex, ivec2(max(0, q.x - uSplit), q.y), 0).b * 255.0;\n }\n if (uKey == 1) {\n float cb = 128.0 - 0.168736 * rgb.r - 0.331264 * rgb.g + 0.5 * rgb.b;\n float cr = 128.0 + 0.5 * rgb.r - 0.418688 * rgb.g - 0.081312 * rgb.b;\n float dist = distance(vec2(cb, cr), uKeyC);\n if (dist < uKeyT.x) { o = vec4(0.0); return; }\n if (dist < uKeyT.y) {\n float k = (dist - uKeyT.x) / (uKeyT.y - uKeyT.x);\n a *= k;\n float avg = (rgb.r + rgb.b) * 0.5;\n if (rgb.g > avg) rgb.g = rgb.g * k + avg * (1.0 - k);\n }\n }\n rgb += vec3(uShift.x, uShift.y, -uShift.x);\n rgb = clamp(rgb, 0.0, 255.0) / 255.0;\n o = vec4(rgb * a, a);\n}";function initPixGL(){if(pixGL.tried)return pixGL.gl;pixGL.tried=!0;try{const e=document.createElement("canvas"),t=e.getContext("webgl2",{premultipliedAlpha:!0,preserveDrawingBuffer:!0,antialias:!1,alpha:!0});if(!t)return null;const n=t.getExtension("WEBGL_debug_renderer_info");if(n&&/swiftshader|llvmpipe|softpipe|software/i.test(t.getParameter(n.UNMASKED_RENDERER_WEBGL)))return null;const i=(e,n)=>{const i=t.createShader(e);if(t.shaderSource(i,n),t.compileShader(i),!t.getShaderParameter(i,t.COMPILE_STATUS))throw new Error(t.getShaderInfoLog(i));return i},r=t.createProgram();if(t.attachShader(r,i(t.VERTEX_SHADER,PIX_VERT)),t.attachShader(r,i(t.FRAGMENT_SHADER,PIX_FRAG)),t.linkProgram(r),!t.getProgramParameter(r,t.LINK_STATUS))throw new Error(t.getProgramInfoLog(r));t.useProgram(r),t.bindBuffer(t.ARRAY_BUFFER,t.createBuffer()),t.bufferData(t.ARRAY_BUFFER,new Float32Array([-1,-1,1,-1,-1,1,1,1]),t.STATIC_DRAW);const a=t.getAttribLocation(r,"aPos");t.enableVertexAttribArray(a),t.vertexAttribPointer(a,2,t.FLOAT,!1,0,0),t.bindTexture(t.TEXTURE_2D,t.createTexture());for(const[e,n]of[[t.TEXTURE_MIN_FILTER,t.NEAREST],[t.TEXTURE_MAG_FILTER,t.NEAREST],[t.TEXTURE_WRAP_S,t.CLAMP_TO_EDGE],[t.TEXTURE_WRAP_T,t.CLAMP_TO_EDGE]])t.texParameteri(t.TEXTURE_2D,e,n);t.pixelStorei(t.UNPACK_FLIP_Y_WEBGL,!1),t.pixelStorei(t.UNPACK_PREMULTIPLY_ALPHA_WEBGL,!1);const o={};for(const e of["uTex","uSize","uSplit","uKey","uKeyC","uKeyT","uShift"])o[e]=t.getUniformLocation(r,e);t.uniform1i(o.uTex,0),e.addEventListener("webglcontextlost",e=>{e.preventDefault(),pixGL.gl=null}),e.addEventListener("webglcontextrestored",()=>{pixGL.tried=!1});const s=document.createElement("canvas");return Object.assign(pixGL,{gl:t,canvas:e,loc:o,stage:s,stageCtx:s.getContext("2d")}),t}catch(e){return console.warn("[FableCut] WebGL2 pixel pass unavailable, using the CPU path:",e&&e.message),null}}function pixelPassGL(e,t,n,i,r,a,o,s,l,c){const d=initPixGL();if(!d)return null;const{stage:u,stageCtx:p,canvas:m,loc:f}=pixGL;u.width<s&&(u.width=s),u.height<l&&(u.height=l),p.clearRect(0,0,s,l),p.drawImage(n,i,r,a,o,0,0,s,l);const h=t.bgRemove?bgSeg.masks.get(e.id):null;h&&h.width&&(p.globalCompositeOperation="destination-in",p.drawImage(h,0,0,s,l),p.globalCompositeOperation="source-over");const g=!!t.chromaKey,v=+t.temperature||0,x=+t.tint||0,b=Math.round(clamp(+t.rgbSplit||0,0,60)*(s/Math.max(1,c))),y=g||v||x||b>0;let T=pixGL.outs.get(e.id);T||(pixGL.outs.size>=32&&pixGL.outs.delete(pixGL.outs.keys().next().value),T=document.createElement("canvas"),pixGL.outs.set(e.id,T)),T.width!==s&&(T.width=s),T.height!==l&&(T.height=l);const k=T.getContext("2d");if(k.clearRect(0,0,s,l),!y)return k.drawImage(u,0,0,s,l,0,0,s,l),T;try{if(m.width<s&&(m.width=s),m.height<l&&(m.height=l),d.viewport(0,0,s,l),d.texImage2D(d.TEXTURE_2D,0,d.RGBA,d.RGBA,d.UNSIGNED_BYTE,u),d.uniform2i(f.uSize,s,l),d.uniform1i(f.uSplit,b),d.uniform1i(f.uKey,g?1:0),g){const[e,n,i]=hexToRgb(t.chromaKey),r=1.2*clamp(+t.chromaTolerance||0,0,100);d.uniform2f(f.uKeyC,128-.168736*e-.331264*n+.5*i,128+.5*e-.418688*n-.081312*i),d.uniform2f(f.uKeyT,r,r+Math.max(1,1.2*clamp(+t.chromaSoftness||0,0,100)))}return d.uniform2f(f.uShift,.6*v,.5*x),d.clearColor(0,0,0,0),d.clear(d.COLOR_BUFFER_BIT),d.drawArrays(d.TRIANGLE_STRIP,0,4),k.drawImage(m,0,m.height-l,s,l,0,0,s,l),T}catch(e){if(e&&"SecurityError"===e.name)throw e;return console.warn("[FableCut] pixel pass failed, using the CPU path:",e&&e.message),pixGL.gl=null,null}}function pixelPass(e,t,n,i,r,a,o,s,l){const c=Math.max(2,Math.min(Math.round(s),1920)),d=Math.max(2,Math.min(Math.round(l),1920)),u=pixelPassGL(e,t,n,i,r,a,o,c,d,s);if(u)return u;scratch.width!==c&&(scratch.width=c),scratch.height!==d&&(scratch.height=d),scratchCtx.clearRect(0,0,c,d),scratchCtx.drawImage(n,i,r,a,o,0,0,c,d);const p=t.bgRemove?bgSeg.masks.get(e.id):null;p&&p.width&&(scratchCtx.globalCompositeOperation="destination-in",scratchCtx.drawImage(p,0,0,c,d),scratchCtx.globalCompositeOperation="source-over");const m=!!t.chromaKey,f=+t.temperature||0,h=+t.tint||0,g=Math.round(clamp(+t.rgbSplit||0,0,60)*(c/Math.max(1,s)));if(m||f||h||g>0){const e=scratchCtx.getImageData(0,0,c,d),n=e.data;if(g>0){const e=new Uint8ClampedArray(n);for(let t=0;t<d;t++){const i=t*c*4;for(let t=0;t<c;t++){const r=i+4*t;n[r]=e[i+4*Math.min(c-1,t+g)],n[r+2]=e[i+4*Math.max(0,t-g)+2]}}}let i=0,r=0,a=0,o=1;if(m){const[e,n,s]=hexToRgb(t.chromaKey);i=128-.168736*e-.331264*n+.5*s,r=128+.5*e-.418688*n-.081312*s,a=1.2*clamp(+t.chromaTolerance||0,0,100),o=a+Math.max(1,1.2*clamp(+t.chromaSoftness||0,0,100))}const s=.6*f,l=.5*h;for(let e=0;e<n.length;e+=4){let t=n[e],c=n[e+1],d=n[e+2];if(m){const s=128-.168736*t-.331264*c+.5*d,l=128+.5*t-.418688*c-.081312*d,u=Math.sqrt((s-i)*(s-i)+(l-r)*(l-r));if(u<a){n[e+3]=0;continue}if(u<o){const i=(u-a)/(o-a);n[e+3]=Math.round(n[e+3]*i);const r=(t+d)/2;c>r&&(c=c*i+r*(1-i))}}s&&(t+=s,d-=s),l&&(c+=l),n[e]=clamp(t,0,255),n[e+1]=clamp(c,0,255),n[e+2]=clamp(d,0,255)}scratchCtx.putImageData(e,0,0)}return scratch}function buildFilter(e){const t=[];return 100!==e.brightness&&t.push(`brightness(${e.brightness}%)`),100!==e.contrast&&t.push(`contrast(${e.contrast}%)`),100!==e.saturation&&t.push(`saturate(${e.saturation}%)`),e.hue&&t.push(`hue-rotate(${e.hue}deg)`),e.blur&&t.push(`blur(${e.blur}px)`),e.grayscale&&t.push(`grayscale(${e.grayscale}%)`),e.sepia&&t.push(`sepia(${e.sepia}%)`),e.invert&&t.push(`invert(${e.invert}%)`),t.length?t.join(" "):"none"}function visibleClipsAt(e){const t=[],n=TRACKS.filter(e=>"video"===e.kind&&isTrackEnabled(e.id)).reverse();for(const i of n)t.push(...project.clips.filter(t=>t.track===i.id&&activeAt(t,e)).sort((e,t)=>e.start-t.start));return t}function drawFrame(e=state.time){const t=els.preview.width,n=els.preview.height;ctx2d.setTransform(1,0,0,1,0,0),ctx2d.filter="none",ctx2d.globalAlpha=1,ctx2d.fillStyle=project.background||"#000",ctx2d.fillRect(0,0,t,n);for(const i of visibleClipsAt(e))drawClip(i,t,n,e);state.exporting||state.playing||drawSelectionOverlay(t,n,e)}const CANVAS_TRANSFORM_ENABLED=!1;function clipBounds(e,t,n,i){const r=n/2+(+t.x||0),a=i/2+(+t.y||0),o=(t.rotation||0)*Math.PI/180,s=+t.scale||1;let l,c;if("text"===e.kind)if(hasTextBox(t))l=+t.boxW/2,c=+t.boxH/2;else{const e=measureTextHalfSize(t);l=e.hw,c=e.hh}else l=n/2*s,c=i/2*s;return{cx:r,cy:a,hw:l,hh:c,rot:o}}function isVisualClip(e){return e&&"adjust"!==e.kind&&"audio"!==e.kind}function overlayHandles(e,t,n){const i=Math.cos(e.rot),r=Math.sin(e.rot),a=(t,n)=>({x:e.cx+t*i-n*r,y:e.cy+t*r+n*i}),o=Math.max(6,t/150),s=Math.max(24,t/34),l=1.4*o,c=e=>({x:clamp(e.x,l,t-l),y:clamp(e.y,l,n-l)});return{hs:o,corners:[[-e.hw,-e.hh],[e.hw,-e.hh],[e.hw,e.hh],[-e.hw,e.hh]].map(([e,t])=>c(a(e,t))),topMid:c(a(0,-e.hh)),rotate:c(a(0,-e.hh-s))}}function drawSelectionOverlay(e,t,n){const i=getClip(state.selId);if(!isVisualClip(i)||!activeAt(i,n)||!isTrackEnabled(i.track))return;const r=clipBounds(i,evalProps(i,n),e,t),a=Math.max(2,e/640),o=overlayHandles(r,e,t),s=o.hs;if(ctx2d.setTransform(1,0,0,1,0,0),ctx2d.save(),ctx2d.lineWidth=a,ctx2d.strokeStyle="#4f8cff",ctx2d.save(),ctx2d.translate(r.cx,r.cy),ctx2d.rotate(r.rot),ctx2d.setLineDash([4*a,3*a]),ctx2d.strokeRect(-r.hw,-r.hh,2*r.hw,2*r.hh),ctx2d.restore(),ctx2d.setLineDash([]),CANVAS_TRANSFORM_ENABLED){ctx2d.beginPath(),ctx2d.moveTo(o.topMid.x,o.topMid.y),ctx2d.lineTo(o.rotate.x,o.rotate.y),ctx2d.stroke(),ctx2d.fillStyle="#ffffff";for(const e of o.corners)ctx2d.beginPath(),ctx2d.rect(e.x-s,e.y-s,2*s,2*s),ctx2d.fill(),ctx2d.stroke();ctx2d.beginPath(),ctx2d.arc(o.rotate.x,o.rotate.y,1.1*s,0,2*Math.PI),ctx2d.fillStyle="#ffce5c",ctx2d.fill(),ctx2d.stroke()}ctx2d.restore()}function canvasPt(e){const t=els.preview.getBoundingClientRect();return{x:(e.clientX-t.left)*(els.preview.width/t.width),y:(e.clientY-t.top)*(els.preview.height/t.height)}}function toLocal(e,t){const n=e.x-t.cx,i=e.y-t.cy,r=Math.cos(-t.rot),a=Math.sin(-t.rot);return{x:n*r-i*a,y:n*a+i*r}}function pickClipAt(e,t,n){const i=visibleClipsAt(state.time).filter(isVisualClip);for(let r=i.length-1;r>=0;r--){const a=i[r],o=clipBounds(a,evalProps(a,state.time),t,n),s=toLocal(e,o);if(Math.abs(s.x)<=o.hw&&Math.abs(s.y)<=o.hh)return a}return null}let canvasDrag=null,canvasDidMove=!1;els.preview.style.touchAction="none",els.preview.addEventListener("pointerdown",e=>{if(e.altKey||1===e.button)return;const t=els.preview.width,n=els.preview.height,i=canvasPt(e),r=getClip(state.selId),a=pickClipAt(i,t,n);if(canvasDrag=null,CANVAS_TRANSFORM_ENABLED&&isVisualClip(r)&&activeAt(r,state.time)){const e=clipBounds(r,evalProps(r,state.time),t,n),o=toLocal(i,e),s=overlayHandles(e,t,n),l=1.8*s.hs;if(Math.hypot(i.x-s.rotate.x,i.y-s.rotate.y)<=l)canvasDrag={mode:"rotate",id:r.id,startRot:+r.props.rotation||0,startAng:Math.atan2(i.y-e.cy,i.x-e.cx)};else if(s.corners.some(e=>Math.abs(i.x-e.x)<=l&&Math.abs(i.y-e.y)<=l))if("text"===r.kind){ensureTextBox(r);const e=clipBounds(r,evalProps(r,state.time),t,n),a=overlayHandles(e,t,n).corners.findIndex(e=>Math.abs(i.x-e.x)<=l&&Math.abs(i.y-e.y)<=l),o=[[-1,-1],[1,-1],[1,1],[-1,1]],[s,c]=o[a>=0?a:0],d=Math.cos(e.rot),u=Math.sin(e.rot),p=-s*e.hw,m=-c*e.hh;canvasDrag={mode:"box",id:r.id,rot:e.rot,dragSX:s,dragSY:c,fix:{x:e.cx+p*d-m*u,y:e.cy+p*u+m*d},aspect:Math.max(.05,2*e.hw/Math.max(1e-6,2*e.hh))}}else canvasDrag={mode:"scale",id:r.id,startScale:+r.props.scale||1,startDist:Math.hypot(o.x,o.y)||1};else a?.id===r.id&&Math.abs(o.x)<=e.hw&&Math.abs(o.y)<=e.hh&&(canvasDrag={mode:"move",id:r.id,startX:+r.props.x||0,startY:+r.props.y||0,startPt:i})}if(!canvasDrag){if(!a)return;a.id!==state.selId&&(selectClip(a.id),renderInspector()),canvasDrag={mode:CANVAS_TRANSFORM_ENABLED?"move":"select",id:a.id,startX:+a.props.x||0,startY:+a.props.y||0,startPt:i}}canvasDrag.clientX=e.clientX,canvasDrag.clientY=e.clientY,canvasDrag.editText=0===e.button&&!state.playing&&!state.exporting,canvasDidMove=!1,"move"===canvasDrag.mode?els.preview.style.cursor="move":"rotate"===canvasDrag.mode&&(els.preview.style.cursor=ROTATE_CURSOR),els.preview.setPointerCapture(e.pointerId),e.preventDefault()});const ROTATE_CURSOR=`url("data:image/svg+xml,${encodeURIComponent("<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'><path d='M10 3a7 7 0 1 1-6.7 9' fill='none' stroke='black' stroke-width='4.5' stroke-linecap='round'/><path d='M10 3a7 7 0 1 1-6.7 9' fill='none' stroke='white' stroke-width='2' stroke-linecap='round'/><path d='M10.5 0.5L14.5 3L10.5 5.5Z' fill='white' stroke='black'/></svg>")}") 10 10, crosshair`;function cursorForHandleAngle(e){return["ew-resize","nwse-resize","ns-resize","nesw-resize"][(Math.round(e/45)%4+4)%4]}function updateCanvasCursor(e){const t=els.preview.width,n=els.preview.height,i=canvasPt(e),r=getClip(state.selId);let a="default";if(isVisualClip(r)&&activeAt(r,state.time)&&!state.playing&&!state.exporting){const e=clipBounds(r,evalProps(r,state.time),t,n),o=overlayHandles(e,t,n),s=1.8*o.hs,l=o.corners.find(e=>Math.abs(i.x-e.x)<=s&&Math.abs(i.y-e.y)<=s),c=toLocal(i,e);CANVAS_TRANSFORM_ENABLED&&Math.hypot(i.x-o.rotate.x,i.y-o.rotate.y)<=s?a=ROTATE_CURSOR:CANVAS_TRANSFORM_ENABLED&&l?a=cursorForHandleAngle(180*Math.atan2(l.y-e.cy,l.x-e.cx)/Math.PI):CANVAS_TRANSFORM_ENABLED&&Math.abs(c.x)<=e.hw&&Math.abs(c.y)<=e.hh?a="move":pickClipAt(i,t,n)&&(a="pointer")}else pickClipAt(i,t,n)&&(a="pointer");els.preview.style.cursor=a}function endCanvasDrag(e){if(!canvasDrag)return;const t=!canvasDidMove&&("move"===canvasDrag.mode||"select"===canvasDrag.mode)&&canvasDrag.editText&&"pointerup"===e.type&&!state.playing&&!state.exporting&&"text"===getClip(canvasDrag.id)?.kind,n=canvasDrag.id,i=canvasDidMove&&"select"!==canvasDrag.mode;canvasDrag=null;try{els.preview.releasePointerCapture(e.pointerId)}catch{}i&&(scheduleSave(),renderInspector()),t&&(selectClip(n),renderInspector(),focusTextContent()),updateCanvasCursor(e)}function drawClip(e,t,n,i){if("adjust"===e.kind)return void drawAdjust(e,t,n,i);const r=evalProps(e,i);if(ctx2d.save(),r._wipe){ctx2d.beginPath();const e=r._wipe;"right"===r._wipeDir?ctx2d.rect(t*e,0,t*(1-e),n):"up"===r._wipeDir?ctx2d.rect(0,0,t,n*(1-e)):"down"===r._wipeDir?ctx2d.rect(0,n*e,t,n*(1-e)):ctx2d.rect(0,0,t*(1-e),n),ctx2d.clip()}if(null!=r._iris&&(ctx2d.beginPath(),ctx2d.arc(t/2,n/2,Math.max(.01,1-r._iris)*Math.hypot(t,n)*.55,0,2*Math.PI),ctx2d.clip()),ctx2d.globalAlpha=clamp(r.opacity,0,1),r.blend&&"normal"!==r.blend&&BLEND_MODES.includes(r.blend)&&(ctx2d.globalCompositeOperation="normal"===r.blend?"source-over":r.blend),ctx2d.translate(t/2+(+r.x||0),n/2+(+r.y||0)),ctx2d.rotate((r.rotation||0)*Math.PI/180),r.shake>0){const e=+r.shake,t=i*clamp(+r.shakeSpeed||8,.5,40)*Math.PI*2;ctx2d.translate(Math.sin(1.3*t)*e*.6+Math.sin(2.71*t)*e*.4,Math.cos(1.7*t)*e*.5+Math.sin(3.13*t)*e*.35),ctx2d.rotate(Math.sin(.9*t)*e*.0022)}if("text"===e.kind)return drawText(e,r,i-e.start),void ctx2d.restore();let a=null,o=0,s=0;if("image"===e.kind?(a=runtime.mediaAux.get(e.mediaId)?.img,a&&(o=a.naturalWidth,s=a.naturalHeight)):"svg"===e.kind?(a=getSvgImage(e,i),a&&(o=a.naturalWidth||a.width,s=a.naturalHeight||a.height)):"video"===e.kind&&(a=optimizedSources?.get(e.id)||exportDecodedFrames.get(e.id)||getClipEl(e),exportDecodedFrames.has(e.id)&&!optimizedSources?.has(e.id)?(o=a.displayWidth,s=a.displayHeight):optimizedSources?.has(e.id)?(o=a.naturalWidth||a.width,s=a.naturalHeight||a.height):a&&!a.seeking&&a.readyState>=2&&(o=a.videoWidth,s=a.videoHeight)),a&&o&&s){const l=o*clamp(+r.cropL||0,0,95)/100,c=s*clamp(+r.cropT||0,0,95)/100,d=Math.max(1,o-l-o*clamp(+r.cropR||0,0,95)/100),u=Math.max(1,s-c-s*clamp(+r.cropB||0,0,95)/100),p=r.scale||1;let m,f;if("cover"===r.fit){const e=Math.max(t/d,n/u)*p;m=d*e,f=u*e}else if("stretch"===r.fit)m=t*p,f=n*p;else if("none"===r.fit)m=d*p,f=u*p;else{const e=Math.min(t/d,n/u)*p;m=d*e,f=u*e}if((r.flipH||r.flipV)&&ctx2d.scale(r.flipH?-1:1,r.flipV?-1:1),r.cornerRadius>0&&(ctx2d.beginPath(),ctx2d.roundRect(-m/2,-f/2,m,f,Math.min(+r.cornerRadius,m/2,f/2)),ctx2d.clip()),r.bgRemove&&"video"===e.kind&&!optimizedSources&&requestMask(e.id,a),r.bgRemove&&"image"===e.kind&&!bgSeg.masks.get(e.id)&&requestMask(e.id,a),ctx2d.filter=buildFilter(r),needsPixelPass(r,e)){const t=pixelPass(e,r,a,l,c,d,u,m,f);ctx2d.drawImage(t,0,0,t.width,t.height,-m/2,-f/2,m,f)}else ctx2d.drawImage(a,l,c,d,u,-m/2,-f/2,m,f);if(ctx2d.filter="none",r.vignette>0){const e=ctx2d.createRadialGradient(0,0,.35*Math.min(m,f),0,0,.55*Math.hypot(m,f));e.addColorStop(0,"rgba(0,0,0,0)"),e.addColorStop(1,`rgba(0,0,0,${.9*clamp(r.vignette/100,0,1)})`),ctx2d.fillStyle=e,ctx2d.fillRect(-m/2,-f/2,m,f)}r.grain>0&&drawGrain(r.grain,-m/2,-f/2,m,f,i)}ctx2d.restore()}let _textDirProbe;els.preview.addEventListener("pointermove",e=>{if(!canvasDrag)return void updateCanvasCursor(e);const t=getClip(canvasDrag.id);if(!t)return;const n=els.preview.width,i=els.preview.height,r=canvasPt(e);if(canvasDidMove||!(Math.hypot(e.clientX-canvasDrag.clientX,e.clientY-canvasDrag.clientY)<3))if("select"!==canvasDrag.mode)if(canvasDidMove||(pushUndo(),canvasDidMove=!0),"move"===canvasDrag.mode)t.props.x=Math.round(canvasDrag.startX+(r.x-canvasDrag.startPt.x)),t.props.y=Math.round(canvasDrag.startY+(r.y-canvasDrag.startPt.y));else if("box"===canvasDrag.mode){const a=canvasDrag.aspect||1,o=e.shiftKey;if(e.ctrlKey||e.metaKey){const e=toLocal(r,clipBounds(t,evalProps(t,state.time),n,i));let s=2*Math.abs(e.x),l=2*Math.abs(e.y);o&&(s/a>=l?l=s/a:s=l*a),t.props.boxW=+clamp(s,20,3*n).toFixed(1),t.props.boxH=+clamp(l,16,3*i).toFixed(1)}else{const e=canvasDrag.fix,s=canvasDrag.rot;let l=r.x-e.x,c=r.y-e.y;const d=Math.cos(-s),u=Math.sin(-s);let p=l*d-c*u,m=l*u+c*d;if(o){const e=Math.abs(p),t=Math.abs(m);e/a>=t?m=(Math.sign(m)||canvasDrag.dragSY)*(e/a):p=(Math.sign(p)||canvasDrag.dragSX)*(t*a)}const f=20,h=16;Math.abs(p)<f&&(p=(Math.sign(p)||canvasDrag.dragSX)*f),Math.abs(m)<h&&(m=(Math.sign(m)||canvasDrag.dragSY)*h),o&&(Math.abs(p)/a>=Math.abs(m)?m=(Math.sign(m)||canvasDrag.dragSY)*(Math.abs(p)/a):p=(Math.sign(p)||canvasDrag.dragSX)*(Math.abs(m)*a)),p=clamp(p,3*-n,3*n),m=clamp(m,3*-i,3*i);const g=Math.cos(s),v=Math.sin(s),x=e.x+p*g-m*v,b=e.y+p*v+m*g;t.props.x=Math.round((e.x+x)/2-n/2),t.props.y=Math.round((e.y+b)/2-i/2),t.props.boxW=+Math.abs(p).toFixed(1),t.props.boxH=+Math.abs(m).toFixed(1)}}else if("scale"===canvasDrag.mode){const e=toLocal(r,clipBounds(t,evalProps(t,state.time),n,i));t.props.scale=clamp(+(canvasDrag.startScale*(Math.hypot(e.x,e.y)/canvasDrag.startDist)).toFixed(3),.05,12)}else{const a=n/2+(+t.props.x||0),o=i/2+(+t.props.y||0);let s=canvasDrag.startRot+180*(Math.atan2(r.y-o,r.x-a)-canvasDrag.startAng)/Math.PI;e.shiftKey&&(s=15*Math.round(s/15)),t.props.rotation=Math.round(s)}else canvasDidMove=!0}),els.preview.addEventListener("pointerup",endCanvasDrag),els.preview.addEventListener("pointercancel",endCanvasDrag);const _textDirCache=new Map,TEXT_DIR_CACHE_MAX=256;function detectTextDirection(e){const t=e&&String(e).trim()?String(e):" ",n=_textDirCache.get(t);if(n)return n;_textDirProbe||(_textDirProbe=document.createElement("p"),_textDirProbe.style.cssText="position:fixed;left:-9999px;visibility:hidden;white-space:nowrap",document.body.appendChild(_textDirProbe)),_textDirProbe.dir="auto",_textDirProbe.textContent=t;const i="rtl"===getComputedStyle(_textDirProbe).direction?"rtl":"ltr";return _textDirCache.size>=256&&_textDirCache.clear(),_textDirCache.set(t,i),i}function lineDirections(e,t){if("rtl"===e.direction||"ltr"===e.direction){const n=e.direction;return t.map(()=>n)}return t.map(e=>detectTextDirection(e))}function revealWipeRect(e,t,n,i,r,a,o=6){const s=(t+2*o)*r,l=a?e+t/2+o-s:e-t/2-o;ctx2d.rect(l,i-n/2,s,n)}function typewriterX(e,t,n,i){const r=(t-n)/2;return i?e+r:e-r}function runOrigin(e,t,n){return n?e+t/2:e-t/2}const SHAPING_SCRIPT_RE=/[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF\u0900-\u097F\u0980-\u09FF\u0A00-\u0A7F\u0A80-\u0AFF\u0B00-\u0B7F\u0B80-\u0BFF\u0C00-\u0C7F\u0C80-\u0CFF\u0D00-\u0D7F]/;function needsContextualShaping(e){return SHAPING_SCRIPT_RE.test(e)}let _graphemeSeg;const _graphemeCache=new Map,_letterAnimCache=new Map,TEXT_SEG_CACHE_MAX=256;function graphemeSegments(e){const t=String(e??""),n=_graphemeCache.get(t);if(n)return n;let i;return"undefined"!=typeof Intl&&Intl.Segmenter?(_graphemeSeg||(_graphemeSeg=new Intl.Segmenter(void 0,{granularity:"grapheme"})),i=[..._graphemeSeg.segment(t)].map(e=>e.segment)):i=[...t],_graphemeCache.size>=256&&_graphemeCache.clear(),_graphemeCache.set(t,i),i}function letterAnimSegments(e){const t=String(e??""),n=_letterAnimCache.get(t);if(n)return n;let i;if(needsContextualShaping(t)){i=[];const e=/(\s+|[^\s]+)/g;let n;for(;n=e.exec(t);)i.push({text:n[0],animate:!!n[0].trim()})}else i=graphemeSegments(t).map(e=>({text:e,animate:!!e.trim()}));return _letterAnimCache.size>=256&&_letterAnimCache.clear(),_letterAnimCache.set(t,i),i}const backOut=e=>{const t=e-1;return 1+2.70158*t*t*t+1.70158*t*t};function hexToRgba(e,t){const n=parseInt(String(e).replace("#",""),16)||0;return`rgba(${n>>16&255},${n>>8&255},${255&n},${t})`}function hasTextBox(e){return+e.boxW>0&&+e.boxH>0}function textJustifyTarget(e,t){if(+e.boxW>0)return+e.boxW;const n=Math.max(.001,+e.scale||1);return Math.max(t,project.width/n*.85)}function justifyLineBySpaces(e,t,n){const i=String(t).split(/\s+/).filter(Boolean);if(i.length<2)return t;const r=i.join(" ");if(e.measureText(r).width>=n-.5)return r;let a=1,o=2,s=r;for(;o<160&&e.measureText(i.join(" ".repeat(o))).width<n;)o*=2;for(;a<=o;){const t=a+o>>1,r=i.join(" ".repeat(t));e.measureText(r).width<=n?(s=r,a=t+1):o=t-1}return s}function textFontWeight(e){return+e.weight||(e.bold?700:400)}function setTextFont(e,t,n){const i=textFontWeight(t);e.font=`${t.italic?"italic ":""}${i} ${n}px "${t.font||"Segoe UI"}", sans-serif`;try{e.letterSpacing=`${+t.letterSpacing||0}px`}catch{}}function textSourceString(e){let t=String(e.text||"");return e.uppercase&&(t=t.split("\n").map(e=>e.toUpperCase()).join("\n")),t}function wrapTextToWidth(e,t,n){const i=[],r=Math.max(1,n);for(const n of String(t).split("\n")){if(!n){i.push("");continue}const t=n.split(/\s+/).filter(Boolean);if(!t.length){i.push("");continue}let a=t[0];for(let n=1;n<t.length;n++){const o=a+" "+t[n];e.measureText(o).width<=r?a=o:(i.push(a),a=t[n])}i.push(a)}return i.length?i:[""]}const lineHeightOf=e=>clamp(+e.lineHeight||1.2,.6,3);function fitFontSizeToBox(e,t,n,i,r){const a=lineHeightOf(t),o="justify"===t.align,s=textSourceString(t);let l=8,c=Math.max(8,Math.round(r||72)),d=8;for(;l<=c;){const r=l+c>>1;setTextFont(e,t,r);let u=wrapTextToWidth(e,s,n);o&&(u=u.map(t=>justifyLineBySpaces(e,t,n))),Math.max(1,u.length)*r*a<=i+.5?(d=r,l=r+1):c=r-1}return d}function measureTextHalfSize(e){ctx2d.save();const t=e.fontSize||72;setTextFont(ctx2d,e,t);let n=textSourceString(e).split("\n");if("justify"===e.align){const t=textJustifyTarget(e,Math.max(1,...n.map(e=>ctx2d.measureText(e).width)));n=n.map(e=>justifyLineBySpaces(ctx2d,e,t))}const i=Math.max(1,...n.map(e=>ctx2d.measureText(e).width)),r=t*lineHeightOf(e);ctx2d.restore();const a=+e.scale||1;return{hw:(i/2+.25*t)*a,hh:(n.length*r/2+.14*t)*a}}function ensureTextBox(e){if("text"!==e.kind||hasTextBox(e.props))return;const t=measureTextHalfSize(e.props),n=+e.props.scale||1;Math.abs(n-1)>.01&&(e.props.fontSize=Math.round((+e.props.fontSize||72)*n),e.props.scale=1),e.props.boxW=Math.max(40,+(2*t.hw).toFixed(1)),e.props.boxH=Math.max(24,+(2*t.hh).toFixed(1))}function drawText(e,t,n){const i=hasTextBox(t),r=+t.boxW,a=+t.boxH,o=i&&!!t.boxFit,s=textSourceString(t),l=textFontWeight(t);let c=t.fontSize||72;i?o&&(c=fitFontSizeToBox(ctx2d,t,r,a,t.fontSize||72)):ctx2d.scale(t.scale||1,t.scale||1),setTextFont(ctx2d,t,c),ctx2d.textBaseline="middle";let d=i?wrapTextToWidth(ctx2d,s,r):s.split("\n");const u="justify"===t.align;if(u){const e=i?r:textJustifyTarget(t,Math.max(1,...d.map(e=>ctx2d.measureText(e).width)));d=d.map(t=>justifyLineBySpaces(ctx2d,t,e))}const p=c*lineHeightOf(t),m=Math.max(1,d.length),f="top"===t.vAlign||"bottom"===t.vAlign?t.vAlign:"middle";let h;h=i?"top"===f?-a/2+p/2:"bottom"===f?a/2-p/2-(m-1)*p:-(m-1)*p/2:-(m-1)*p/2;const g=TEXT_ANIMS.includes(t.textAnim)?t.textAnim:"none",v=clamp(+t.wordRate||.15,.03,2),x="left"===t.align||"right"===t.align?t.align:"center",b=d.map(e=>ctx2d.measureText(e).width),y=i?r:Math.max(1,...b),T=lineDirections(t,d),k=e=>"left"===x?-y/2+b[e]/2:"right"===x?y/2-b[e]/2:0;i&&(ctx2d.beginPath(),ctx2d.rect(-r/2,-a/2,r,a),ctx2d.clip());const S=(0===t.textShadow?0:+t.textShadow||12)*c/100;if(t.bgOpacity>0){ctx2d.fillStyle=hexToRgba(t.bgColor||"#000",clamp(t.bgOpacity,0,1));const e=.4*c,n=.18*c,i=.28*c;d.forEach((t,r)=>{if(!t.trim())return;const a=b[r],o=h+r*p;ctx2d.beginPath(),ctx2d.roundRect(k(r)-a/2-e,o-p/2-n+.08*p,a+2*e,p+2*n-.16*p,i),ctx2d.fill()})}const w=e=>{if(!t.color2)return t.color||"#fff";const n=ctx2d.createLinearGradient(0,e-.55*c,0,e+.55*c);return n.addColorStop(0,t.color||"#fff"),n.addColorStop(1,t.color2),n},E=(e,n,i,r=1,a=!1)=>{if(r<=0)return;const o=ctx2d.globalAlpha,s=ctx2d.direction;ctx2d.globalAlpha=o*clamp(r,0,1),ctx2d.direction=a?"rtl":"ltr",ctx2d.textAlign="center",t.strokeWidth>0&&(ctx2d.shadowColor="transparent",ctx2d.lineJoin="round",ctx2d.miterLimit=2,ctx2d.lineWidth=t.strokeWidth,ctx2d.strokeStyle=t.strokeColor||"#000",ctx2d.strokeText(e,n,i)),t.glow>0?(ctx2d.shadowColor=t.glowColor||t.color||"#fff",ctx2d.shadowBlur=+t.glow*c/40,ctx2d.fillStyle=w(i),ctx2d.fillText(e,n,i),ctx2d.fillText(e,n,i)):S>0?(ctx2d.shadowColor="rgba(0,0,0,.7)",ctx2d.shadowBlur=S,ctx2d.shadowOffsetY=S/3,ctx2d.fillStyle=w(i),ctx2d.fillText(e,n,i)):(ctx2d.fillStyle=w(i),ctx2d.fillText(e,n,i)),ctx2d.shadowColor="transparent",ctx2d.shadowBlur=0,ctx2d.shadowOffsetY=0,ctx2d.direction=s,ctx2d.globalAlpha=o};if("none"===g)return void d.forEach((e,t)=>E(e,k(t),h+t*p,1,"rtl"===T[t]));if("clip-reveal"===g)return void d.forEach((e,t)=>{if(!e.trim())return;const i=clamp((n-t*v)/.5,0,1);if(i<=0)return;const r=EASE["ease-out"](i),a=b[t],o=k(t),s=h+t*p,l="rtl"===T[t];ctx2d.save(),ctx2d.beginPath(),revealWipeRect(o,a,p,s,r,l),ctx2d.clip(),E(e,o,s,1,l),ctx2d.restore()});if("zoom-in"===g)return void d.forEach((e,t)=>{if(!e.trim())return;const i=clamp((n-t*v)/.45,0,1);if(i<=0)return;const r=1.35-.35*EASE["ease-out"](i),a="rtl"===T[t];ctx2d.save(),ctx2d.translate(k(t),h+t*p),ctx2d.scale(r,r),E(e,0,0,Math.min(1,1.6*i),a),ctx2d.restore()});if("rise-mask"===g)return void d.forEach((e,t)=>{if(!e.trim())return;const i=clamp((n-t*v)/.5,0,1);if(i<=0)return;const r=EASE["ease-out"](i),a=k(t),o=h+t*p,s="rtl"===T[t];ctx2d.save(),ctx2d.beginPath(),ctx2d.rect(a-y/2-24,o-p/2,y+48,p),ctx2d.clip(),E(e,a,o+(1-r)*p,1,s),ctx2d.restore()});if("font-cut"===g){const e=Array.isArray(t.fontCutSet)&&t.fontCutSet.length?t.fontCutSet:FONT_CUT_DEFAULT,i=.6,r=.06;let a=t.font||"Segoe UI";n<i&&(a=e[Math.floor(n/r)%e.length]),ctx2d.font=`${t.italic?"italic ":""}${l} ${c}px "${a}", sans-serif`;const o=d.map(e=>ctx2d.measureText(e).width),s=Math.max(1,...o),u=e=>"left"===x?-s/2+o[e]/2:"right"===x?s/2-o[e]/2:0;return void d.forEach((e,t)=>{e.trim()&&E(e,u(t),h+t*p,1,"rtl"===T[t])})}if("typewriter"===g){let e=Math.floor(n/(v/4));return void d.forEach((t,n)=>{const i="rtl"===T[n],r=graphemeSegments(t),a=r.slice(0,Math.max(0,e)).join("");if(e-=r.length,a){const e=ctx2d.measureText(a).width;E(a,typewriterX(k(n),b[n],e,i),h+n*p,1,i)}})}if("letter-pop"===g||"wave"===g){let e=0;return void d.forEach((t,i)=>{const r=h+i*p,a="rtl"===T[i],o=needsContextualShaping(t),s=letterAnimSegments(t),l=o?v:v/3,d=s.map(e=>ctx2d.measureText(e.text).width),u=d.reduce((e,t)=>e+t,0);let m=runOrigin(k(i),u,a);s.forEach((t,i)=>{const o=d[i],s=a?m-o/2:m+o/2;if(t.animate){if("letter-pop"===g){const i=clamp((n-e*l)/.18,0,1);if(i>0){ctx2d.save(),ctx2d.translate(s,r);const e=Math.max(.001,backOut(i));ctx2d.scale(e,e),E(t.text,0,0,Math.min(1,2.5*i),a),ctx2d.restore()}}else E(t.text,s,r+Math.sin(4*n+.55*e)*c*.12,1,a);e++}m+=a?-o:o})})}const M=ctx2d.measureText(" ").width;let C=0;d.forEach((e,t)=>{const i=h+t*p,r="rtl"===T[t],a=e.split(/\s+/).filter(Boolean),o=a.map(e=>ctx2d.measureText(e).width),s=o.reduce((e,t)=>e+t,0),l=Math.max(0,a.length-1),d=u&&l?b[t]:s+M*l,m=l?(d-s)/l:M;let f=runOrigin(k(t),d,r);a.forEach((e,t)=>{const a=o[t],s=r?f-a/2:f+a/2,l=clamp((n-C*v)/.25,0,1);if("word-pop"===g){if(l>0){ctx2d.save(),ctx2d.translate(s,i);const t=Math.max(.001,backOut(l));ctx2d.scale(t,t),E(e,0,0,Math.min(1,2.5*l),r),ctx2d.restore()}}else"word-slide"===g?l>0&&E(e,s,i+(1-EASE["ease-out"](l))*c*.7,l,r):"bounce"===g?E(e,s,i-Math.abs(Math.sin(3.2*n+.9*C))*c*.18,1,r):"shake"===g?E(e,s+Math.sin(31*n+7.3*C)*c*.035,i+Math.cos(27*n+3.1*C)*c*.035,1,r):E(e,s,i,l>=1?1:.3+.7*l,r);f+=r?-(a+m):a+m,C++})})}async function loadLibraryFonts(){if(runtime.fontsLoading)return runtime.fontsLoading;runtime.fontsLoading=(async()=>{try{const e=await(await fetch("/api/library?dir=fonts")).json();for(const t of e){if(!/\.(ttf|otf|woff2?)$/i.test(t.name))continue;const e=t.name.replace(/\.[^.]+$/,"");if(!runtime.customFonts.includes(e))try{const n=new FontFace(e,`url("${t.src}")`);await n.load(),document.fonts.add(n),runtime.customFonts.push(e)}catch{}}runtime.customFonts.sort()}catch{}})();try{await runtime.fontsLoading}finally{runtime.fontsLoading=null}}let lastTs=null;function loop(e){null==lastTs&&(lastTs=e);const t=Math.min(.1,(e-lastTs)/1e3);lastTs=e,updateScrub(t),updateClipDragScroll(t);const n=projDur();let i=!0;if(!state.rendering){const r=playStopAt(n);runtime.previewWait&&runtime.previewWait.anchor!==state.time&&resetPreviewWait();const a=runtime.previewWait?.t??previewNextTime(t,r),o=previewFrameTime(a,n),s=preparePreviewMedia(a,n);if(!s.length||state.exporting&&!backgroundExportActive)resetPreviewWait(),state.time=a,state.playing&&!runtime.scrub&&state.time>=r&&(state.exporting&&!backgroundExportActive?finishExport(!0):pause()),syncMedia();else{i=!1,silencePreviewMedia();const t=JSON.stringify(project.clips.filter(e=>("video"===e.kind||"audio"===e.kind)&&isTrackEnabled(e.track)&&activeAt(e,o)).map(e=>[e.id,runtime.previewMedia.get(e.id)?.key]));let n=runtime.previewWait;n&&n.signature===t&&n.t===a||(n=runtime.previewWait={t:a,anchor:state.time,since:e,signature:t}),!n.reported&&e-n.since>=200&&(els.btnPlay.title=uiText("Buffering…"),els.btnPlay.textContent="⏳");const r=s.find(e=>e.error);if(r||e-n.since>=15e3){const{c:e,error:t}=r||s[0];n.reported||(pause(),n.reported=!0,runtime.previewWait=n,toast(uiText("Video preview: {name}: {error}",{name:getMedia(e.mediaId)?.name||e.mediaId||e.id,error:t||uiText("loading timed out")})))}}i&&drawFrame(previewFrameTime(state.time,n))}if(state.playing&&!runtime.scrub&&!1!==runtime.followPlayhead){const e=state.time*state.pps,t=els.timelineScroll;(e<t.scrollLeft||e>t.scrollLeft+t.clientWidth-40)&&scrollTimeline(Math.max(0,e-60))}state.dirtyTimeline&&rebuildClips(),updateClipThumbs(),updateMissingMediaNotice(),els.playhead.style.left=state.time*state.pps+"px",drawRuler(),updateSafeOverlay(),updateKfGraphs(),updateMeterUI(t),els.tcCurrent.textContent=fmt(state.time),els.tcTotal.textContent=fmt(n),!state.exporting||state.rendering||backgroundExportActive||realtimeStarting||realtimeFinishing||updateExportProgress(n?state.time/n*100:0,uiText("Quick export in progress. Keep this page open and in the foreground.")),requestAnimationFrame(loop)}let exportOpening=!1,realtimeStarting=!1,realtimeFinishing=!1,exportPreviewUrl="",exportDetails=null,exportResult=null,exportSaveController=null;function showExportDialog(){rememberExportDialog({open:!0}),els.exportOverlay.classList.remove("hidden"),$("btnCloseExport").focus()}function hideExportDialog(){rememberExportDialog({open:!1}),dismissExportCancellation(),exportSaveController?.abort(),$("exportVideo").pause(),els.exportOverlay.classList.add("hidden"),$("btnExport").focus()}function cancelExport(){if(backgroundExportActive)renderCancelled=!0,cancelBackgroundExport();else if(state.rendering&&!realtimeStarting)renderCancelled=!0,optimizedAbort?.abort(),fastExportAbort?.abort();else if(state.exporting)finishExport(!1);else{const e=readRememberedBackgroundExport();if(!e)return;backgroundExportId=e,backgroundExportActive=state.exporting=renderCancelled=!0,cancelBackgroundExport().then(e=>endBackgroundExportUi(e))}}function dismissExportCancellation(){$("cancelExportDialog").open&&$("cancelExportDialog").close()}function exportNeedsCancellation(){const e=readExportDialog();return!("complete"===e.phase||"error"===e.phase&&e.canRecord)&&(state.exporting||realtimeStarting||realtimeFinishing||!!readRememberedBackgroundExport())}function closeExportDialog(){if(exportNeedsCancellation())return $("cancelExportDialog").open||$("cancelExportDialog").showModal(),void $("btnContinueExport").focus();forgetBackgroundExport(),hideExportDialog()}function confirmCancelExport(){exportNeedsCancellation()?cancelExport():forgetBackgroundExport(),hideExportDialog()}function updateExportProgress(e,t,n="working"){const i="complete"===n?100:Math.max(0,Math.min(99,Number(e)||0));rememberExportDialog({phase:n,percent:i,message:t}),els.exportOverlay.dataset.status=n,els.exportProgress.style.width=i.toFixed(1)+"%",$("exportPercent").textContent=Math.floor(i)+"%",$("exportProgressTrack").setAttribute("aria-valuenow",Math.floor(i)),els.exportNote.title="",els.exportNote.textContent!==t&&(els.exportNote.textContent=t)}function syncExportTrackWarning(){const e=$("exportTrackWarn"),t=TRACKS.filter(e=>!isTrackEnabled(e.id)&&project.clips.some(t=>t.track===e.id)).map(e=>e.id);if(t.length&&e){const n=t.join(", ");e.textContent=1===t.length?uiText("Track {tracks} is disabled and will be omitted from the export.",{tracks:n}):uiText("Tracks {tracks} are disabled and will be omitted from the export.",{tracks:n}),e.classList.remove("hidden")}else e&&(e.textContent="",e.classList.add("hidden"))}function resetExportDialog(e=null){$("exportChecks")&&($("exportChecks").hidden=!0,$("exportChecks").open=!1),exportSaveController?.abort(),exportSaveController=null,exportResult=null,$("btnSaveExport").disabled=!1,$("exportSaveLabel").textContent=uiText("Save"),setExportSaveNote("");const t=$("exportVideo");t.pause(),t.removeAttribute("src"),t.removeAttribute("poster"),t.load(),t.controls=!1,exportPreviewUrl&&URL.revokeObjectURL(exportPreviewUrl),exportPreviewUrl="",exportDetails=e||{name:project.name||uiText("Untitled Project"),duration:Math.max(1/project.fps,Math.round(projDur()*project.fps)/project.fps),width:project.width,height:project.height,fps:project.fps,clips:project.clips.filter(e=>isVisualClip(e)&&isTrackEnabled(e.track)).map(e=>({start:e.start,duration:e.duration,kind:e.kind})),renderStartedAt:0};const{duration:n,width:i,height:r}=exportDetails;e?.poster&&(t.poster=e.poster),els.exportTitle.textContent=exportDetails.name,els.exportTitle.title=exportDetails.name,$("exportDuration").textContent=uiText("Video duration {seconds}s",{seconds:n.toFixed(1)}),$("exportTime").textContent=`${String(Math.floor(n/60)).padStart(2,"0")}:${(n%60).toFixed(1).padStart(4,"0")}`;const a=i/r;$("exportPreview").style.setProperty("--export-aspect",`${i} / ${r}`),$("exportPreview").style.setProperty("--export-preview-width",`min(${Math.min(476,398*a)}px, ${Math.min(58,58*a)}dvh)`),$("btnExportPreview").hidden=!1,$("btnExportPreview").disabled=!0,$("exportTime").hidden=!1;for(const e of["btnExportFallback","btnRetryExport","btnCancelExport","btnSaveExport"])$(e).hidden=!0;syncExportTrackWarning(),rememberBackgroundExportDetails(),updateExportProgress(0,uiText("Preparing video…")),showExportDialog()}let coverCaptureQueue=Promise.resolve(),coverStillActive=!1;function captureTimelineStill(e,t=720,n=()=>!0){const i=coverCaptureQueue.then(()=>n()?captureTimelineStillNow(e,t):null);return coverCaptureQueue=i.catch(()=>{}),i}async function captureTimelineStillNow(e,t=720){const n=state.time,i=state.rendering,r=state.exporting;state.rendering=!0,state.exporting=!0,coverStillActive=!0;try{await loadLibraryFonts(),await document.fonts.ready,await seekVideosTo(e),await Promise.all(visibleClipsAt(e).filter(e=>"image"===e.kind||"svg"===e.kind).map(async e=>{const t=getMedia(e.mediaId);if(t)if("image"!==e.kind||runtime.mediaAux.get(t.id)?.img)"svg"!==e.kind||runtime.mediaAux.get(t.id)?.svgText||await loadSvgMedia(t);else{const e=await loadImage(t.src);runtime.mediaAux.set(t.id,{...runtime.mediaAux.get(t.id),img:e})}})),await prepareFrameAssets(e),drawFrame(e);const n=document.createElement("canvas");return n.width=Math.min(t,els.preview.width),n.height=Math.round(n.width*els.preview.height/els.preview.width),n.getContext("2d").drawImage(els.preview,0,0,n.width,n.height),n.toDataURL("image/jpeg",.9)}finally{coverStillActive=!1,state.time=n,state.rendering=i,state.exporting=r&&!renderCancelled,seekMediaWhilePaused()}}async function coverPoster(e,t=720,n=()=>!0){if("image"===e?.type){const n=await loadImage(e.src),i=document.createElement("canvas");i.width=Math.min(t,project.width),i.height=Math.max(1,Math.round(i.width*project.height/project.width));const r=i.getContext("2d"),a=Math.min(i.width/n.width,i.height/n.height);return r.fillStyle=project.background||"#000",r.fillRect(0,0,i.width,i.height),r.drawImage(n,(i.width-n.width*a)/2,(i.height-n.height*a)/2,n.width*a,n.height*a),i.toDataURL("image/jpeg",.9)}const i=Math.max(0,(Math.max(1,Math.round(projDur()*project.fps))-1)/project.fps);return captureTimelineStill(clamp(e?.time||0,0,i),t,n)}async function captureExportPoster(){try{$("exportVideo").poster=await coverPoster(project.cover),rememberBackgroundExportDetails()}catch{}}let coverDraft=null,coverSession=0,coverRequest=0,coverFrameTime=0,coverButtonKey="",coverDraftReady=!1,coverUploading=!1;function coverThumbnailKey(){const e=project.media.map(({id:e,kind:t,src:n,duration:i,width:r,height:a})=>({id:e,kind:t,src:n,duration:i,width:r,height:a}));return JSON.stringify([activeProjectId,project.cover,project.clips,e,project.tracks,project.width,project.height,project.fps,project.background,project.disabledTracks,[...mediaSourceVersions]])}async function coverThumbnailStorage(e){return withClipThumbStorage(async()=>{const t=clamp(project.cover?.time||0,0,Math.max(0,(Math.round(projDur()*project.fps)-1)/project.fps)),n="image"===project.cover?.type?[project.cover.src]:visibleClipsAt(t).map(e=>getMedia(e.mediaId)?.src).filter(Boolean),i=await Promise.all([...new Set(n)].map(clipThumbSourceVersion));if(i.some(e=>!e))return null;const r=(new TextEncoder).encode(JSON.stringify([e,i])),a=[...new Uint8Array(await crypto.subtle.digest("SHA-256",r))].map(e=>e.toString(16).padStart(2,"0")).join("");return{cache:await caches.open("fablecut-cover-thumbs-v1"),key:location.origin+"/__fablecut_cover__/"+a}})}function storeCoverThumbnail(e,t){e&&validStoredClipThumb(t)&&withClipThumbStorage(async()=>{await e.cache.put(e.key,new Response(t));const n=await e.cache.keys();for(const t of n.slice(0,Math.max(0,n.length-32)))await e.cache.delete(t)})}let coverButtonPending=null;function refreshCoverButton(){if($("btnDeleteCover").hidden=!project.cover,!getSetting("showCover"))return void(coverButtonPending=null);const e=coverThumbnailKey(),t=$("coverThumbnail");if(e===coverButtonKey)return coverButtonPending=null,void(t.hidden=!project.cover);if(e===coverButtonPending?.key)return;if(t.hidden=!0,!project.cover)return coverButtonPending=null,void(coverButtonKey=e);const n=coverButtonPending={key:e},i=()=>coverButtonPending===n&&e===coverThumbnailKey()&&getSetting("showCover");(async()=>{try{const n=await coverThumbnailStorage(e);if(!i())return;const r=n&&await withClipThumbStorage(async()=>{const e=await n.cache.match(n.key),t=e&&await e.text();if(validStoredClipThumb(t))try{return await loadImage(t),t}catch{}return e&&await n.cache.delete(n.key),null});if(!i())return;let a=r;if(!a){for(;i()&&(state.playing||state.rendering||state.exporting||state.gesture||exportOpening||document.hidden||$("coverDialog").open);)await new Promise(e=>setTimeout(e,100));if(!i())return;a=await coverPoster(project.cover,120,i)}if(!a||!i())return;t.src=a,t.hidden=!1,coverButtonKey=e,r||storeCoverThumbnail(n,a)}catch{}finally{coverButtonPending===n&&(coverButtonPending=null,getSetting("showCover")&&project.cover&&e!==coverThumbnailKey()&&refreshCoverButton())}})()}function updateCoverControls(){const e="image"!==coverDraft?.type;$("btnCoverFrame").setAttribute("aria-pressed",String(e)),$("btnCoverImage").setAttribute("aria-pressed",String(!e)),$("btnCoverFrame").disabled=projDur()<=0||coverUploading,$("btnCoverImage").disabled=coverUploading,$("btnSetCover").disabled=!coverDraftReady||coverUploading,$("coverTimeline").classList.toggle("inactive",!e||coverUploading),$("coverTimeline").setAttribute("aria-disabled",String(!e||coverUploading||projDur()<=0)),$("coverTimeline").setAttribute("aria-valuemax",String(lastCoverFrameTime())),$("coverTimeline").setAttribute("aria-valuenow",String(coverFrameTime)),drawCoverPlayhead()}async function previewCoverDraft(){const e=coverSession,t=++coverRequest;coverDraftReady=!1,updateCoverControls(),$("coverStatus").textContent=uiText("Loading cover…");try{const n=await coverPoster(coverDraft,720,()=>e===coverSession&&t===coverRequest&&$("coverDialog").open);if(e!==coverSession||t!==coverRequest||!$("coverDialog").open)return;$("coverPreview").src=n,$("coverPreview").hidden=!1,$("coverStatus").textContent="",coverDraftReady=!0}catch{if(e!==coverSession||t!==coverRequest)return;$("coverPreview").hidden=!0,$("coverStatus").textContent=uiText("Cover could not be loaded")}updateCoverControls()}function lastCoverFrameTime(){return Math.max(0,(Math.max(1,Math.round(projDur()*project.fps))-1)/project.fps)}function selectCoverFrame(e){coverFrameTime=clamp(Math.round(e*project.fps)/project.fps,0,lastCoverFrameTime()),coverDraft={type:"frame",time:coverFrameTime},previewCoverDraft()}function drawCoverPlayhead(){if(!$("coverDialog").open)return;const e=$("coverFrames").clientWidth,t=e/Math.max(projDur(),.001),n=coverFrameTime;$("coverPlayhead").style.left=n*t+"px",$("coverRuler").style.width=`${e}px`,$("coverRuler").style.height="26px",drawRulerMainThread(e,26,window.devicePixelRatio||1,$("coverRuler"),0,t,n,!1)}function renderCoverTimeline(){if(!$("coverDialog").open)return;const e=$("coverFrames"),t=e.clientWidth,n=t/Math.max(projDur(),.001),i=[...new Set(project.clips.filter(e=>"video"===e.kind||"image"===e.kind).map(e=>e.track))].filter(isTrackEnabled).sort((e,t)=>TRACKS.findIndex(t=>t.id===e)-TRACKS.findIndex(e=>e.id===t));e.replaceChildren();for(const r of i){const i=document.createElement("div");i.className="track",i.style.height="64px";for(const e of project.clips.filter(e=>e.track===r&&("video"===e.kind||"image"===e.kind))){const r=document.createElement("div");r.className=`clip c-${e.kind}`,r.style.left=e.start*n+"px",r.style.width=e.duration*n+"px";const a=document.createElement("div");if(a.className="clip-body","video"===e.kind)a.innerHTML=clipThumbStrip(e,64,n,{scrollLeft:0,clientWidth:t});else{const t=document.createElement("img");t.src=mediaLoadSrc(getMedia(e.mediaId)||{src:""}),t.alt="",a.append(t)}r.append(a),i.append(r)}e.append(i)}drawCoverPlayhead(),loadClipThumbs()}async function openCoverDialog(){!coverStillActive||exportOpening||backgroundExportActive||await coverCaptureQueue,state.rendering||state.exporting||state.gesture||$("coverDialog").open||(pause(),++coverSession,coverDraft=project.cover?structuredClone(project.cover):{type:"frame",time:state.time},coverFrameTime=clamp(coverDraft.time||0,0,lastCoverFrameTime()),"frame"===coverDraft.type&&(coverDraft.time=coverFrameTime),$("coverPreview").hidden=!0,$("coverDialog").showModal(),renderCoverTimeline(),$("btnCancelCover").focus(),await previewCoverDraft())}document.addEventListener("pointerdown",e=>{const t=$("coverControl"),n=document.activeElement;!t.contains(e.target)&&t.contains(n)&&n.blur()},!0),new ResizeObserver(()=>{$("coverDialog").open&&renderCoverTimeline()}).observe($("coverFrames")),$("coverTimeline").addEventListener("pointerdown",e=>{if(0!==e.button||coverUploading||"image"===coverDraft?.type||projDur()<=0)return;e.preventDefault();const t=$("coverTimeline"),n=e=>{const t=$("coverFrames").getBoundingClientRect();selectCoverFrame(clamp((e.clientX-t.left)/t.width*projDur(),0,lastCoverFrameTime()))};n(e);try{t.setPointerCapture(e.pointerId)}catch{}const i=t=>{t.pointerId===e.pointerId&&n(t)},r=n=>{n.pointerId===e.pointerId&&(t.removeEventListener("pointermove",i),t.removeEventListener("lostpointercapture",r),t.removeEventListener("pointerup",r),t.removeEventListener("pointercancel",r),t.hasPointerCapture(e.pointerId)&&t.releasePointerCapture(e.pointerId))};t.addEventListener("pointermove",i),t.addEventListener("pointerup",r),t.addEventListener("pointercancel",r),t.addEventListener("lostpointercapture",r)}),$("btnCover").addEventListener("click",openCoverDialog);for(const e of["btnCloseCover","btnCancelCover"])$(e).addEventListener("click",()=>$("coverDialog").close());function clearCover(){project.cover&&(pushUndo(),project.cover=null,scheduleSave())}function failExportDialog(e,t=!0,n=parseFloat(els.exportProgress.style.width)){dismissExportCancellation();const{engine:i}=readExportDialog(),r="realtime"===i||"local"===i;let a=uiText("Export interrupted. Try quick export to export again.");t?"realtime"===i?a=uiText("Background recording failed. You can record in this page while it stays open."):"local"===i&&(a=uiText("Recording failed. Keep this page open to try again.")):a=uiText("Export failed: ")+e,updateExportProgress(n,a,"error"),els.exportNote.title=e||"",$("btnExportFallback").hidden=!t,$("btnExportFallback").querySelector("span").textContent=uiText(r?"Record in this page":"Quick export"),$("btnRetryExport").hidden=t,$("btnCancelExport").hidden=!0,rememberExportDialog({error:e,canRecord:t})}function setExportSaveNote(e,t=""){const n=$("exportSaveNote");n.textContent=e,n.hidden=!e,n.title=t,n.classList.toggle("error",!!t)}async function saveCompletedExport(){if(!exportResult||exportSaveController)return;const e=exportResult,t=new AbortController;exportSaveController=t,$("btnSaveExport").disabled=!0,$("exportSaveLabel").textContent=uiText("Saving…"),setExportSaveNote("");try{await saveExportInBrowser(e,t.signal)}catch(n){e!==exportResult||t.signal.aborted||setExportSaveNote(uiText("Unable to save. Click Save to try again."),n.message)}finally{exportSaveController===t&&(exportSaveController=null,$("btnSaveExport").disabled=!1,$("exportSaveLabel").textContent=uiText("Save"))}}async function saveExportInBrowser(e,t){t.throwIfAborted();const n=e.filename||decodeURIComponent(e.src.split("/").pop()),i=document.createElement("a");i.href=e.src,i.download=n,i.click(),e!==exportResult||t.aborted||setExportSaveNote(uiText("Check your browser downloads to confirm the file was saved."))}function completeExportDialog(e,t="",n){dismissExportCancellation();const i=readExportDialog();exportResult={src:e,filename:t};const r=$("exportChecks"),a=n||i.outputs||[];if(r){r.hidden=!a.length;const e=$("exportCheckResults");e.replaceChildren();for(const t of a){const n=document.createElement("p");if(t.src){const e=document.createElement("a");e.href=t.src,e.download=t.name||"",e.textContent=t.name||decodeURIComponent(t.src.split("/").pop()),n.append(e,":")}n.append(document.createTextNode(t.error||t.report?.line||uiText("Checks unavailable"))),e.append(n)}}updateExportProgress(100,uiText("Video complete. Your file is ready."),"complete"),$("btnCancelExport").hidden=!0,$("exportVideo").src=e,$("btnExportPreview").disabled=!1,$("btnSaveExport").hidden=!1,rememberExportDialog({src:e,filename:t,outputs:a}),new URLSearchParams(location.search).has("exportJob")||i.savePrompted&&i.src===e||(rememberExportDialog({savePrompted:!0}),saveCompletedExport())}function updateBackgroundExportProgress(e){const t=e.totalFrames?(e.frames||0)/e.totalFrames*100:0;let n=uiText("Preparing video…");if("queued"===e.state)n=uiText("Queued for export…");else if("finalizing"===e.state)n=uiText("Finalizing video…");else if("rendering"===e.state){const i=Math.max(0,((e.frames||1)-1)/exportDetails.fps),r=exportDetails.clips.filter(e=>"video"===e.kind),a=(r.length?r:exportDetails.clips).slice().sort((e,t)=>e.start-t.start),o=a.findIndex(e=>e.start<=i&&e.start+e.duration>i);if(n=o<0?uiText("Rendering video…"):uiText("Rendering clip {current}/{total}",{current:o+1,total:a.length}),exportDetails.renderStartedAt||(exportDetails.renderStartedAt=Date.now()),t>0&&t<99&&Date.now()-exportDetails.renderStartedAt>2e3){const e=Math.ceil((Date.now()-exportDetails.renderStartedAt)/1e3*(100-t)/t);n+=uiText(" · About {seconds}s remaining",{seconds:e})}e.wait&&(n=uiText("Preparing the next part of your video. Export will continue automatically.")),"realtime"===e.engine&&(n=uiText("Quick export in progress. You can switch chats."))}updateExportProgress("finalizing"===e.state?99:t,n)}$("coverDialog").addEventListener("keydown",e=>e.stopPropagation()),$("coverDialog").addEventListener("close",()=>{$("coverDialog").open||(coverSession++,coverRequest++,coverUploading=!1,$("coverFrames").replaceChildren(),state.dirtyTimeline=!0,$("coverFile").value="",refreshCoverButton(),$("btnCover").focus(),runtime.pendingSync&&syncFromServer())}),$("coverTimeline").addEventListener("keydown",e=>{if(coverUploading||"image"===coverDraft?.type||projDur()<=0)return;const t={ArrowLeft:-1,ArrowDown:-1,ArrowRight:1,ArrowUp:1};(e.key in t||"Home"===e.key||"End"===e.key)&&(e.preventDefault(),selectCoverFrame("Home"===e.key?0:"End"===e.key?lastCoverFrameTime():coverFrameTime+t[e.key]/project.fps))}),$("btnCoverFrame").addEventListener("click",()=>selectCoverFrame(coverFrameTime)),$("btnCoverImage").addEventListener("click",()=>$("coverFile").click()),$("coverFile").addEventListener("change",async()=>{const e=$("coverFile").files[0];if($("coverFile").value="",!e)return;if(!/\.(png|jpe?g|webp|gif)$/i.test(e.name))return void toast(uiText("Choose a PNG, JPEG, WebP or GIF image"));const t=coverSession;let n;coverUploading=!0,coverRequest++,coverDraftReady=!1,updateCoverControls(),$("coverStatus").textContent=uiText("Loading cover…");try{n=URL.createObjectURL(e),await loadImage(n);const i=await fetch(projectApi("/api/upload?name="+encodeURIComponent("cover-"+uid()+"-"+e.name)),{method:"POST",body:e});if(!i.ok)throw new Error("upload");const r=await i.json();if(t!==coverSession||!$("coverDialog").open)return;coverDraft={type:"image",src:r.src},coverUploading=!1,await previewCoverDraft()}catch{if(t!==coverSession)return;$("coverStatus").textContent=uiText("Cover image upload failed")}finally{n&&URL.revokeObjectURL(n),t===coverSession&&(coverUploading=!1,updateCoverControls())}}),$("btnSetCover").addEventListener("click",()=>{if(!coverDraftReady||coverUploading)return;pushUndo(),project.cover=structuredClone(coverDraft);const e=$("coverPreview").src,t=coverThumbnailKey();$("coverThumbnail").src=e,$("coverThumbnail").hidden=!1,coverButtonKey=t,Promise.all([coverThumbnailStorage(t),loadImage(e)]).then(([e,t])=>{const n=document.createElement("canvas");n.width=Math.min(120,t.width),n.height=Math.max(1,Math.round(n.width*t.height/t.width)),n.getContext("2d").drawImage(t,0,0,n.width,n.height),storeCoverThumbnail(e,n.toDataURL("image/jpeg",.9))}).catch(()=>{}),scheduleSave(),$("coverDialog").close()}),$("btnDeleteCover").addEventListener("click",e=>{e.stopPropagation(),clearCover(),$("btnCover").focus()}),$("btnResetCover").addEventListener("click",()=>{clearCover(),$("coverDialog").close()});let backgroundExportId=null,backgroundExportActive=!1,backgroundExportCancellation=null;function cancelBackgroundExport(){return backgroundExportId?(backgroundExportCancellation||(backgroundExportCancellation=fetch(projectApi("/api/export/cancel?id="+backgroundExportId),{method:"POST"}).then(e=>e.ok||404===e.status).catch(()=>!1)),backgroundExportCancellation):Promise.resolve(!1)}function backgroundExportStorageKey(){return"fablecut.backgroundExport:"+activeProjectId}function readExportDialog(){try{return JSON.parse(sessionStorage.getItem(backgroundExportStorageKey()+":dialog"))||{}}catch{return{}}}function rememberExportDialog(e){try{sessionStorage.setItem(backgroundExportStorageKey()+":dialog",JSON.stringify({...readExportDialog(),...e}))}catch{}}function rememberBackgroundExportDetails(){if(exportDetails)try{sessionStorage.setItem(backgroundExportStorageKey()+":details",JSON.stringify({...exportDetails,poster:$("exportVideo").poster,renderStartedAt:0}))}catch{}}function rememberBackgroundExport(e){try{const t=backgroundExportStorageKey();sessionStorage.setItem(t,e),rememberBackgroundExportDetails()}catch{}}function forgetBackgroundExport(){try{sessionStorage.removeItem(backgroundExportStorageKey()),sessionStorage.removeItem(backgroundExportStorageKey()+":details"),sessionStorage.removeItem(backgroundExportStorageKey()+":dialog")}catch{}}function readBackgroundExportDetails(){try{const e=JSON.parse(sessionStorage.getItem(backgroundExportStorageKey()+":details"));return e&&"string"==typeof e.name&&Array.isArray(e.clips)&&[e.duration,e.fps,e.width,e.height].every(e=>Number.isFinite(e)&&e>0)?e:null}catch{return null}}function readRememberedBackgroundExport(){try{const e=sessionStorage.getItem(backgroundExportStorageKey());return/^[a-f0-9]{32}$/.test(e||"")?e:""}catch{return""}}function beginBackgroundExportUi(e=null){backgroundExportActive=!0,state.exporting=!0,renderCancelled=!1,resetExportDialog(e)}function endBackgroundExportUi(e=!0){backgroundExportId=null,backgroundExportActive=!1,backgroundExportCancellation=null,state.exporting=!1,e&&forgetBackgroundExport(),renderCancelled&&hideExportDialog(),runtime.pendingSync&&syncFromServer()}async function followBackgroundExport(e){backgroundExportId=e,rememberBackgroundExport(e);let t=!1;try{for(;!renderCancelled;){const n=await fetch(projectApi("/api/export/status?id="+e),{cache:"no-store"}),i=await n.json();if(renderCancelled)break;if(!n.ok)throw new Error(i.error||"Export status unavailable");if(t=["complete","error","cancelled"].includes(i.state),"error"===i.state){failExportDialog(i.error||"",!0,i.totalFrames?(i.frames||0)/i.totalFrames*100:void 0);break}if("cancelled"===i.state){renderCancelled=!0;break}if("complete"===i.state){if(!i.src)throw new Error(uiText("The exported file is unavailable. Please retry."));completeExportDialog(i.src,"",i.outputs);break}updateBackgroundExportProgress(i),await new Promise(e=>setTimeout(e,500))}}catch(e){renderCancelled||failExportDialog(e.message,t)}finally{renderCancelled&&(t=await cancelBackgroundExport()),endBackgroundExportUi(renderCancelled&&t)}}async function backgroundExport(e,t={}){if(state.exporting)return;pause();const n="realtime"===e?readBackgroundExportDetails():null;forgetBackgroundExport(),beginBackgroundExportUi(n),rememberExportDialog({engine:e});let i,r="";try{if("realtime"!==e&&await captureExportPoster(),runtime.savePromise&&await runtime.savePromise,runtime.saveTimer&&(clearTimeout(runtime.saveTimer),runtime.saveTimer=null,await saveProjectNow()),renderCancelled)return;"realtime"===e&&(i=Array.from(crypto.getRandomValues(new Uint8Array(16)),e=>e.toString(16).padStart(2,"0")).join(""),rememberBackgroundExport(i),rememberExportDialog({submittedAt:Date.now()}));const n=await fetch(projectApi("/api/export/start"),{method:"POST",keepalive:!!i,headers:{"Content-Type":"application/json"},body:JSON.stringify({engine:e,revision:project.revision,requestId:i,fallback:!0===t.fallback,...["optimized","fast"].includes(e)&&!project.delivery&&"delivery"!==getSetting("exportProfile")?{profile:getSetting("exportProfile")}:{}})}),a=await n.json();if(!n.ok)throw i&&(rememberBackgroundExport(""),i=null),new Error(a.error||"Export start failed");r=a.id}catch(e){renderCancelled?i&&(r=i):failExportDialog(e.message,!i)}finally{r||endBackgroundExportUi(renderCancelled)}r&&await followBackgroundExport(r)}async function resumeBackgroundExport(e=!1){const t=new URLSearchParams(location.search);if(state.exporting||t.has("exportJob")||t.has("cliExport"))return!1;const n=readRememberedBackgroundExport(),i=readExportDialog();if(!(n||e&&i.open))return!1;renderCancelled=!1;const r=readBackgroundExportDetails();if(!n)return resetExportDialog(r),"complete"===i.phase&&i.src&&!i.src.startsWith("blob:")?completeExportDialog(i.src,i.filename,i.outputs):failExportDialog(i.error||uiText("The exported file is unavailable. Please retry."),!0,i.percent),!0;let a;try{const e=()=>fetch(projectApi("/api/export/status?id="+n),{cache:"no-store"});let t=await e();for(let n=0;n<12&&!renderCancelled&&404===t.status&&Date.now()-i.submittedAt<3e3;n++)await new Promise(e=>setTimeout(e,250)),t=await e();if(a=await t.json(),renderCancelled)return!0;if(404===t.status)return forgetBackgroundExport(),resetExportDialog(r),"realtime"===i.engine&&rememberExportDialog({engine:i.engine}),failExportDialog(uiText("The exported file is unavailable. Please retry.")),!0;if(!t.ok)throw new Error(a.error||uiText("Unable to check export progress. Please retry."))}catch(e){return renderCancelled||(resetExportDialog(r),failExportDialog(uiText("Unable to check export progress. Please retry."),!1)),!0}return"cancelled"===a.state?(forgetBackgroundExport(),!1):"complete"===a.state?(pause(),resetExportDialog(r),r||await captureExportPoster(),renderCancelled?(forgetBackgroundExport(),!0):(a.src?completeExportDialog(a.src,"",a.outputs):failExportDialog(uiText("The exported file is unavailable. Please retry.")),!0)):"error"===a.state?(pause(),resetExportDialog(r),r||await captureExportPoster(),failExportDialog(a.error||"",!0,a.totalFrames?(a.frames||0)/a.totalFrames*100:void 0),!0):(beginBackgroundExportUi(r),pause(),r||await captureExportPoster(),await followBackgroundExport(n),!0)}async function restoreExportDialog(){const e=new URLSearchParams(location.search);if(!(!state.connected||state.exporting||exportOpening||e.has("exportJob")||e.has("cliExport"))&&readExportDialog().open){exportOpening=!0;try{await resumeBackgroundExport(!0)}finally{exportOpening=!1}}}async function openExportButton(){if(!coverStillActive||exportOpening||backgroundExportActive||await coverCaptureQueue,state.exporting)showExportDialog();else if(!(exportOpening||realtimeStarting||realtimeFinishing)){exportOpening=!0;try{if(await resumeBackgroundExport())return;if(!project.clips.length)return void alert(uiText("Timeline is empty — add some clips first."));const e=getSetting("exportEngine");await backgroundExport(["optimized","fast","webcodecs"].includes(e)?e:"webcodecs")}finally{exportOpening=!1}}}let renderCancelled=!1,fastExportAbort=null;async function prepareVideoFrame(e,t,n){const i=i=>new Error(`Video "${getMedia(e.mediaId)?.name||e.mediaId}" (clip ${e.id}, source ${n.toFixed(4)}s): ${i} (readyState=${t?.readyState}, size=${t?.videoWidth}x${t?.videoHeight}, currentTime=${t?.currentTime})`);if(!t)throw i("missing media");t.paused||t.pause();const r=(e,n,r,a=()=>{})=>new Promise((o,s)=>{const l=["loadedmetadata","loadeddata","canplay","seeked","error"],c=e=>{clearTimeout(u);for(const e of l)t.removeEventListener(e,d);e?s(e):o()},d=()=>t.error?c(i(`decode failed: ${t.error.message||"media error "+t.error.code}`)):!(t.readyState>=1)||t.videoWidth&&t.videoHeight?void(e()&&c()):c(i("no decodable video track; use a supported codec or transcode the source to H.264")),u=setTimeout(()=>c(i(`${r} timed out after ${n/1e3}s`)),n);for(const e of l)t.addEventListener(e,d);try{a(),d()}catch(e){c(i(e.message))}});await r(()=>t.readyState>=1,15e3,"metadata load"),await r(()=>!t.seeking&&t.readyState>=2&&Math.abs(t.currentTime-n)<1e-4,1e4,"frame seek",()=>{Math.abs(t.currentTime-n)>=1e-4&&(t.currentTime=n)})}function seekVideosTo(e){const t=project.clips.filter(t=>"video"===t.kind&&isTrackEnabled(t.track)&&activeAt(t,e));for(const n of t)stealExportVideo(n,e);return releaseFinishedExportVideos(e),Promise.all(t.map(async t=>{await decodeExportVideo(t,e)||await prepareVideoFrame(t,acquireExportVideo(t,e),mediaTimeAt(t,e))}))}function encodeWAV(e){const t=e.numberOfChannels,n=e.length,i=e.sampleRate,r=44+n*t*2,a=new ArrayBuffer(r),o=new DataView(a),s=(e,t)=>{for(let n=0;n<t.length;n++)o.setUint8(e+n,t.charCodeAt(n))};s(0,"RIFF"),o.setUint32(4,r-8,!0),s(8,"WAVE"),s(12,"fmt "),o.setUint32(16,16,!0),o.setUint16(20,1,!0),o.setUint16(22,t,!0),o.setUint32(24,i,!0),o.setUint32(28,i*t*2,!0),o.setUint16(32,2*t,!0),o.setUint16(34,16,!0),s(36,"data"),o.setUint32(40,n*t*2,!0);const l=[];for(let n=0;n<t;n++)l.push(e.getChannelData(n));let c=44;for(let e=0;e<n;e++)for(let n=0;n<t;n++){const t=clamp(l[n][e],-1,1);o.setInt16(c,t<0?32768*t:32767*t,!0),c+=2}return new Blob([a],{type:"audio/wav"})}function wsolaStretchChannels(e,t,n,i){const r=e.length,a=Array.from({length:r},()=>new Float32Array(n));if(!r||n<=0)return a;const o=e[0].length;if(!o)return a;let s=Math.round(.03*t);s%2!=0&&s++;const l=s>>1,c=Math.round(.012*t);if(n<=l){const s=Math.round(i(0)*t);for(let t=0;t<r;t++){const i=e[t],r=a[t];for(let e=0;e<n;e++){const t=s+e;t>=0&&t<o&&(r[e]=i[t])}}return a}const d=new Float32Array(s);for(let e=0;e<s;e++)d[e]=.5*(1-Math.cos(2*Math.PI*e/s));let u=i(0),p=u;const m=Math.min(64,Math.max(8,Math.ceil(n/l)));for(let e=1;e<=m;e++){const r=i(e/m*(n/t));r<u&&(u=r),r>p&&(p=r)}const f=Math.max(0,Math.floor(u*t)),h=Math.min(o,Math.ceil(p*t)),g=Math.max(0,f-c-s),v=Math.min(o,h+c+2*s),x=Math.max(0,v-g),b=new Float32Array(x);if(1===r)b.set(e[0].subarray(g,v));else{const t=1/r;for(let n=0;n<r;n++){const i=e[n].subarray(g,v);for(let e=0;e<x;e++)b[e]+=i[e]*t}}const y=Math.round(i(0)*t);let T=y;for(let t=0;t<r;t++){const i=e[t],r=a[t];for(let e=0;e<l&&e<n;e++){const t=y+e;t>=0&&t<o&&(r[e]=i[t])}for(let e=l;e<s&&e<n;e++){const t=y+e;t>=0&&t<o&&(r[e]=i[t]*d[e])}}for(let u=l;u<n;u+=l){const p=Math.round(i(u/t)*t),m=T+l-g;let f=p;if(m>=0&&m+l<=x){let e=0;for(let t=0;t<l;t+=2){const n=b[m+t];e+=n*n}if(e>=1e-8){let t=0,n=-2;const i=-c,r=c;for(let a=i;a<=r;a+=4){const i=p+a;if(i<0||i+l>o)continue;const r=i-g;if(r<0||r+l>x)continue;let s=0,c=0;for(let e=0;e<l;e+=2){const t=b[m+e],n=b[r+e];s+=t*n,c+=n*n}const d=s/Math.sqrt(c*e+1e-12);d>n&&(n=d,t=a)}let a=t,s=-2;const d=Math.max(i,t-3),u=Math.min(r,t+3);for(let t=d;t<=u;t++){const n=p+t;if(n<0||n+l>o)continue;const i=n-g;if(i<0||i+l>x)continue;let r=0,c=0;for(let e=0;e<l;e++){const t=b[m+e],n=b[i+e];r+=t*n,c+=n*n}const d=r/Math.sqrt(c*e+1e-12);d>s&&(s=d,a=t)}f=p+a}}for(let t=0;t<r;t++){const i=e[t],r=a[t];for(let e=0;e<s;e++){const t=u+e;if(t>=n)break;const a=f+e;a>=0&&a<o&&(r[t]+=i[a]*d[e])}}T=f}return a}function stretchClipAudio(e,t,n){const i=n.sampleRate,r=n.numberOfChannels,a=Math.max(1,Math.ceil(t.duration*i)),o=[];for(let e=0;e<r;e++)o.push(n.getChannelData(e));const s=wsolaStretchChannels(o,i,a,e=>mediaTimeAt(t,t.start+e)),l=e.createBuffer(r,a,i);for(let e=0;e<r;e++)l.getChannelData(e).set(s[e]);return l}async function getExportAudioBuffer(e,t){if(!/^\/(?:projects\/|media\/|library\/)/.test(e.src))return getAudioBuffer(e).catch(()=>null);const n=await fetch(projectApi("/api/export/source-audio?src="+encodeURIComponent(e.src)),{signal:t});if(204===n.status)return null;if(!n.ok){const t=await n.json().catch(()=>({}));throw new Error(`Audio "${e.name||e.id}": ${t.error||"source decode failed"}`)}return getDecodeCtx().decodeAudioData(await n.arrayBuffer())}async function renderAudioMix(e,t){const n=[],i=new Map,r=new AbortController,a=()=>r.abort();let o;t?.addEventListener("abort",a,{once:!0}),t?.aborted&&a();try{for(const e of project.clips){if("audio"!==e.kind&&"video"!==e.kind)continue;if(!isTrackEnabled(e.track))continue;if(0===e.props?.volume&&!e.keyframes?.volume?.length)continue;const t=getMedia(e.mediaId);t&&(i.has(t.id)||i.set(t.id,getExportAudioBuffer(t,r.signal)),n.push(i.get(t.id).then(t=>t?{c:e,buf:t}:null)))}o=(await Promise.all(n)).filter(Boolean)}catch(e){throw a(),e}finally{t?.removeEventListener("abort",a)}if(!o.length)return null;const s=new OfflineAudioContext(2,Math.ceil(48e3*e)+1,48e3);for(const{c:e,buf:t}of o){const n=s.createBufferSource(),i=s.createGain(),r=Math.max(2,Math.ceil(30*e.duration)),a=new Float32Array(r);let o=0;for(let t=0;t<r;t++){const n=clamp(evalProps(e,e.start+t/(r-1)*e.duration).volume,0,4);a[t]=n,n>o&&(o=n)}i.gain.setValueCurveAtTime(a,Math.max(0,e.start),Math.max(.01,e.duration));const l=e.props?.audioChannel,{out:c}=connectChannelIsolated(s,n,i,l);c.connect(s.destination);const d=clipSpeed(e),u=hasSpeedRamp(e);(u||Math.abs(d-1)>1e-6)&&o>1e-6?(n.buffer=stretchClipAudio(s,e,t),n.playbackRate.value=1,n.start(Math.max(0,e.start),0,e.duration)):u?(n.buffer=t,n.playbackRate.value=1,n.start(Math.max(0,e.start),Math.max(0,e.in),e.duration)):(n.buffer=t,n.playbackRate.value=d,n.start(Math.max(0,e.start),Math.max(0,e.in),e.duration*d))}return encodeWAV(await s.startRendering())}async function prepareFrameAssets(e){for(const t of project.clips)if(activeAt(t,e)&&isTrackEnabled(t.track)&&("svg"===t.kind&&await prepareSvgFrame(t,e),t.props?.bgRemove&&("video"===t.kind||"image"===t.kind))){const e="video"===t.kind?optimizedSources?.get(t.id)||getClipEl(t):runtime.mediaAux.get(t.mediaId)?.img;if(e)try{await requestMask(t.id,optimizedSources?.has(t.id)?optimizedMaskSource(e):e,!0)}catch{}}}function readBrowserGpu(){try{const e=document.createElement("canvas"),t=e.getContext("webgl")||e.getContext("experimental-webgl");if(!t)return"disabled";const n=t.getExtension("WEBGL_debug_renderer_info");if(!n)return"unknown";const i=t.getParameter(n.UNMASKED_VENDOR_WEBGL)||"",r=t.getParameter(n.UNMASKED_RENDERER_WEBGL)||"";return String(i+"/"+r).slice(0,180)}catch{return""}}function reportExportDiagnostic(e,t){e&&fetch(projectApi("/api/export/report"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({requestId:e,diagnostic:!0,browserGpu:runtime.browserGpu,...t})}).catch(()=>{})}function installExportPageGuards(e){e&&!runtime.exportGuards&&(runtime.exportGuards=!0,runtime.browserGpu=readBrowserGpu(),window.addEventListener("error",t=>{reportExportDiagnostic(e,{error:t.message||"window error",stack:t.error?.stack,stage:"browser"})}),window.addEventListener("unhandledrejection",t=>{const n=t.reason;reportExportDiagnostic(e,{error:n?.message||String(n),stack:n?.stack,stage:"browser"})}),reportExportDiagnostic(e,{browserGpu:runtime.browserGpu}))}async function trackExportWait(e,t,n){const i=performance.now();try{return await n()}finally{const n=performance.now()-i;n>=250&&reportExportDiagnostic(e,{wait:{reason:t,elapsedMs:n},timeSec:state.time})}}async function fastExport(e={}){if(state.exporting)return;pause(),state.exporting=!0,state.rendering=!0,renderCancelled=!1,els.exportOverlay.classList.remove("hidden"),els.exportProgress.style.width="0%",els.exportNote.textContent=uiText("Rendering frames → ffmpeg. You can switch tabs; export continues.");const t=new AbortController;let n,i;fastExportAbort=t;try{installExportPageGuards(e.requestId);const r=exportWindow(e);exportDecodeBegin(t.signal,e.decode),els.exportTitle.textContent=uiText("Mixing audio…");const a=await trackExportWait(e.requestId,"audio-mix",()=>renderExportAudio(r.start,r.dur,t.signal)),o=await fetch(projectApi("/api/export/begin"),{method:"POST",body:JSON.stringify(exportBeginOptions(e,r,a)),signal:t.signal}),s=await o.json();if(!o.ok||!s.id)throw new Error(s.error||"export begin failed");if(n=s.id,a){const e=await fetch("/api/export/audio?id="+n,{method:"POST",body:a,signal:t.signal});if(!e.ok)throw new Error((await e.json()).error||"audio upload failed")}await loadLibraryFonts(),await document.fonts.ready,i=createExportPipeline(s,t.signal,async(e,i)=>{const r=await fetch(`/api/export/frame?id=${n}&frames=${i}`,{method:"POST",body:e,signal:t.signal});if(!r.ok)throw new Error((await r.json()).error||"frame upload failed")});for(let n=0;n<r.frames;n++){if(renderCancelled||t.signal.aborted)throw new Error("cancelled");const a=r.start+n/r.fps;state.time=a,await trackExportWait(e.requestId,"video-seek",()=>seekVideosTo(a)),await trackExportWait(e.requestId,"frame-assets",()=>prepareFrameAssets(a)),drawFrame(a),await i.pipeline.push(els.preview);const o=(n+1)/r.frames*100;els.exportProgress.style.width=o.toFixed(1)+"%",els.exportTitle.textContent=uiText("Rendering… {percent}%",{percent:o.toFixed(0)})}await i.pipeline.finish(),await i.queue.finish(),els.exportTitle.textContent=uiText("Encoding…");const l=await fetch("/api/export/end?id="+n,{method:"POST",signal:t.signal}).then(e=>e.json());if(!l.src)throw new Error(l.error||"encode failed");if(n=null,!e.requestId)for(const e of l.outputs||[l])if(e.src){const t=document.createElement("a");t.href=e.src,t.download=decodeURIComponent(e.src.split("/").pop()),t.click()}}catch(t){const n=renderCancelled||"AbortError"===t.name?"cancelled":String(t.message||t);e.requestId&&await fetch(projectApi("/api/export/report"),{method:"POST",body:JSON.stringify({requestId:e.requestId,group:e.group||0,error:n,stack:t.stack,timeSec:state.time,browserGpu:runtime.browserGpu})}).catch(()=>{}),e.requestId||"cancelled"===n||alert(uiText("Export failed: ")+n)}finally{i?.queue.cancel(),t.abort(),i?.encoder.close(),await(i?.pipeline.settle()),n&&await fetch("/api/export/end?id="+n+"&discard=1",{method:"POST"}).catch(()=>{}),exportDecodeEnd(),fastExportAbort=null,state.exporting=!1,state.rendering=!1,els.exportOverlay.classList.add("hidden"),runtime.pendingSync&&syncFromServer()}}let recorder=null,recChunks=[],recDiscard=!1;function pickMime(){return["video/mp4;codecs=avc1.42E01E,mp4a.40.2","video/mp4","video/webm;codecs=vp9,opus","video/webm;codecs=vp8,opus","video/webm"].find(e=>window.MediaRecorder&&MediaRecorder.isTypeSupported(e))||""}async function startExport(e=!1,t){if(state.exporting||realtimeStarting||realtimeFinishing)return;if(!project.clips.length)return void alert(uiText("Timeline is empty — add some clips first."));const n=pickMime();if(pause(),forgetBackgroundExport(),resetExportDialog(),e&&rememberExportDialog({engine:"local"}),updateExportProgress(0,uiText("Quick export in progress. Keep this page open and in the foreground.")),!n)return void failExportDialog(uiText("MediaRecorder is not supported in this browser."),!1);let i;state.exporting=!0,renderCancelled=!1,realtimeStarting=!0;try{if(ensureAudio(),await runtime.audio.ctx.resume(),await captureExportPoster(),!state.exporting||renderCancelled)return;if(state.time=0,resetPreviewWait(!0),seekMediaWhilePaused(),await new Promise(e=>setTimeout(e,350)),!state.exporting||renderCancelled)return;i=els.preview.captureStream(project.fps);for(const e of runtime.audio.recDest.stream.getAudioTracks())i.addTrack(e.clone());recChunks=[],recDiscard=!1,recorder=new MediaRecorder(i,{mimeType:n,videoBitsPerSecond:1e7}),recorder.ondataavailable=e=>{e.data.size&&recChunks.push(e.data)},recorder.onerror=e=>{finishExport(!1),failExportDialog(e.error?.message||e.type),showExportDialog()},recorder.onstop=()=>{for(const e of i.getTracks())e.stop();if(recorder=null,realtimeFinishing=!1,state.exporting=!1,runtime.pendingSync&&syncFromServer(),recDiscard)return void(recChunks=[]);if(!recChunks.length)return void failExportDialog(uiText("The exported file is unavailable. Please retry."));const e=n.startsWith("video/mp4")?"mp4":"webm";exportPreviewUrl=URL.createObjectURL(new Blob(recChunks,{type:n.split(";")[0]})),recChunks=[];let r=(t||exportDetails.name).replace(/[<>:"/\\|?*\x00-\x1f]+/g,"_").trim().replace(/\.(mp4|webm)$/i,"").slice(0,120).replace(/^[. ]+|[. ]+$/g,"")||"export";/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(r)&&(r="_"+r),completeExportDialog(exportPreviewUrl,r+"."+e)},runtime.audio.monitor.gain.value=0,recorder.start(250),resetPreviewWait(!0),lastTs=null,state.playing=!0,els.btnPlay.textContent="⏸",els.btnPlay.classList.add("on")}catch(e){finishExport(!1);for(const e of i?.getTracks()||[])e.stop();failExportDialog(e.message),showExportDialog()}finally{realtimeStarting=!1}}function finishExport(e){if(state.exporting&&!backgroundExportActive)if(realtimeFinishing)e||(recDiscard=!0,renderCancelled=!0,els.exportOverlay.classList.add("hidden"));else{realtimeFinishing=!(!recorder||"inactive"===recorder.state),state.exporting=realtimeFinishing,!realtimeFinishing&&runtime.pendingSync&&syncFromServer(),recDiscard=!e,renderCancelled=!e,state.playing=!1,els.btnPlay.textContent="▶",els.btnPlay.classList.remove("on");for(const e of runtime.clipEls.values())e.paused||e.pause();for(const e of runtime.clipGain.values())e.gain.value=0;runtime.audio&&(runtime.audio.monitor.gain.value=1),recorder&&"inactive"!==recorder.state&&recorder.stop(),e?updateExportProgress(99,uiText("Quick export in progress. Keep this page open and in the foreground.")):"error"!==els.exportOverlay.dataset.status&&els.exportOverlay.classList.add("hidden")}}async function runRealtimeJob(e,t){try{await startExport(!1,t);const e=Math.max(1,Math.round(projDur()*project.fps));let n=-1;for(;state.exporting;){const t=Math.min(e,Math.floor(state.time*project.fps));if(t!==n){const i=await fetch(projectApi(`/api/export/recording?frames=${t}&totalFrames=${e}`),{method:"POST"});if(!i.ok)throw new Error((await i.json()).error);n=t}await new Promise(e=>setTimeout(e,500))}if(!exportResult)throw new Error(els.exportNote.title||"Recording did not complete");const i=await(await fetch(exportResult.src)).blob(),r=await fetch(projectApi("/api/export/recording?name="+encodeURIComponent(exportResult.filename)),{method:"POST",body:i});if(!r.ok)throw new Error((await r.json()).error)}catch(t){await fetch(projectApi("/api/export/report"),{method:"POST",body:JSON.stringify({requestId:e,error:t.message,stack:t.stack,browserGpu:runtime.browserGpu})})}}function settingsFocusables(){const e=$("settingsDialog");return e?[...e.querySelectorAll("button, [href], input, select, textarea, [tabindex]:not([tabindex='-1'])")].filter(e=>!e.disabled&&e.getClientRects().length):[]}function onSettingsTabTrap(e){if("Tab"!==e.key)return;const t=settingsFocusables();if(!t.length)return;const n=t[0],i=t[t.length-1];e.shiftKey&&document.activeElement===n?(e.preventDefault(),i.focus()):e.shiftKey||document.activeElement!==i||(e.preventDefault(),n.focus())}function syncExportSettings(){$("setExportEngine").value=getSetting("exportEngine"),$("setExportProfile").value=getSetting("exportProfile"),$("setExportProfile").disabled="webcodecs"===getSetting("exportEngine")}function openSettings(){applyInterfaceVisibility();const e=$("setLinkSelect");e&&(e.checked=!!getSetting("linkSelect")),$("setLanguage").value=getSetting("language"),syncExportSettings();const t=$("settingsOverlay");t.classList.remove("hidden"),t.addEventListener("keydown",onSettingsTabTrap);const n=$("settingsDialog");($("setLanguage")||n)?.focus()}function closeSettings(){const e=$("settingsOverlay");e.removeEventListener("keydown",onSettingsTabTrap),e.classList.add("hidden"),($("btnSettings").getClientRects().length?$("btnSettings"):$("btnExport"))?.focus()}els.fileInput.addEventListener("change",()=>{importFiles(els.fileInput.files),els.fileInput.value=""}),$("btnTitle").addEventListener("click",addTitle),$("btnAdjust").addEventListener("click",addAdjust),$("btnSplit").addEventListener("click",splitAtPlayhead),$("btnUndo").addEventListener("click",undo),$("btnRedo").addEventListener("click",redo),$("btnCloseGap").addEventListener("click",()=>closeGapAtPlayhead()),$("btnNextGap").addEventListener("click",goToNextGap),$("btnTrimIO").addEventListener("click",trimToWorkArea),$("btnWorkAreaPlay").addEventListener("click",()=>{state.workAreaPlay=!state.workAreaPlay,syncTrimIOButton()}),$("btnDelete").addEventListener("click",()=>{clearFocusedTransition()||deleteSelected()}),$("btnExport").addEventListener("click",()=>{openExportButton()}),$("btnCloseExport").addEventListener("click",closeExportDialog),$("btnContinueExport").addEventListener("click",dismissExportCancellation),$("btnConfirmCancelExport").addEventListener("click",confirmCancelExport),$("cancelExportDialog").addEventListener("cancel",e=>{e.preventDefault(),dismissExportCancellation()}),$("cancelExportDialog").addEventListener("keydown",e=>{e.stopPropagation()}),$("btnRetryExport").addEventListener("click",()=>{openExportButton()}),$("btnExportFallback").addEventListener("click",()=>{!state.connected||["realtime","local"].includes(readExportDialog().engine)?startExport(!0):backgroundExport("realtime",{fallback:!0})}),$("btnSaveExport").addEventListener("click",()=>{saveCompletedExport()}),$("btnExportPreview").addEventListener("click",async()=>{const e=$("exportVideo");try{await e.play(),e.controls=!0,$("btnExportPreview").hidden=!0,$("exportTime").hidden=!0}catch{toast(uiText("Unable to preview. Open the downloaded video to watch it."))}}),$("exportVideo").addEventListener("ended",()=>{$("exportVideo").controls=!1,$("btnExportPreview").hidden=!1,$("exportTime").hidden=!1}),els.exportOverlay.addEventListener("keydown",e=>{if("Escape"===e.key&&(e.preventDefault(),e.stopPropagation(),closeExportDialog()),"Tab"!==e.key)return;const t=[...$("exportDialog").querySelectorAll("button, video[controls]")].filter(e=>!e.disabled&&e.getClientRects().length),n=t[0],i=t[t.length-1];e.shiftKey&&document.activeElement===n?(e.preventDefault(),i.focus()):e.shiftKey||document.activeElement!==i||(e.preventDefault(),n.focus())}),$("btnCancelExport").addEventListener("click",closeExportDialog),$("btnPlay").addEventListener("click",()=>state.playing?pause():play()),els.btnSpeed.addEventListener("click",()=>cyclePreviewRate(1)),$("btnHome").addEventListener("click",gotoHome),$("btnEnd").addEventListener("click",gotoEnd),$("btnBack").addEventListener("click",()=>setTime(state.time-1/project.fps)),$("btnFwd").addEventListener("click",()=>setTime(state.time+1/project.fps)),$("btnHelp").addEventListener("click",()=>$("helpOverlay").classList.remove("hidden")),$("btnCloseHelp").addEventListener("click",()=>$("helpOverlay").classList.add("hidden")),$("btnSettings").addEventListener("click",openSettings),$("btnCloseSettings").addEventListener("click",closeSettings),$("settingsOverlay").addEventListener("click",e=>{e.target===$("settingsOverlay")&&closeSettings()}),$("setExportProfile").addEventListener("change",e=>setSetting("exportProfile",e.target.value)),$("setExportEngine").addEventListener("change",e=>{setSetting("exportEngine",e.target.value),syncExportSettings()}),$("setLanguage").addEventListener("change",e=>{setSetting("language",FableCutI18n.normalize(e.target.value)),applyInterfaceLanguage()});for(const e of document.querySelectorAll("[data-visibility-setting]"))e.addEventListener("change",()=>setSetting(e.dataset.visibilitySetting,e.checked));function applyInterfaceLanguage(){FableCutI18n.setLanguage(getSetting("language")),FableCutI18n.apply(document),$("setLanguage").value=FableCutI18n.language,els.projectName.textContent=uiText(state.connected?"🟢 connected":"⚪ local session"),closeStylePicker(),runtime.mediaMenu?.close(),closeBinCtxMenu(),renderBin(),renderLibrary(),renderInspector();for(const e of TRACKS)syncTrackDisabledUI(e.id);state.dirtyTimeline=!0,syncAspectSel(),syncFpsSel(),syncExportTrackWarning(),buildMeterDOM(),els.btnPlay.title=uiText(runtime.previewWait?"Buffering…":"Play / Pause (Space)")}function syncSnapButton(){els.btnSnap.classList.toggle("on",state.snap),els.btnSnap.setAttribute("aria-pressed",String(state.snap)),els.btnSnap.dataset.i18nTitle=state.snap?"Snapping on (N)":"Snapping off (N)",els.btnSnap.title=uiText(els.btnSnap.dataset.i18nTitle)}function closeBinCtxMenu(){runtime.binCtxMenu&&(runtime.binCtxMenu.remove(),runtime.binCtxMenu=null,document.removeEventListener("pointerdown",onBinCtxDoc,!0))}function onBinCtxDoc(e){runtime.binCtxMenu&&!runtime.binCtxMenu.contains(e.target)&&closeBinCtxMenu()}function openProjectTabMenu(e,t){closeBinCtxMenu();const n=document.createElement("div");n.className="ctx-menu";const i=document.createElement("div");i.className="ctx-opt",i.textContent=uiText("New folder"),i.addEventListener("click",()=>{closeBinCtxMenu(),"project"!==state.binTab&&setBinTab("project"),addFolder(null)}),n.appendChild(i),document.body.appendChild(n);const r=n.offsetWidth,a=n.offsetHeight;n.style.left=Math.min(e,window.innerWidth-r-6)+"px",n.style.top=Math.min(t,window.innerHeight-a-6)+"px",runtime.binCtxMenu=n,document.addEventListener("pointerdown",onBinCtxDoc,!0)}function syncAspectSel(){if(!els.aspectSel)return;const e=ASPECT_PRESETS.findIndex(e=>e.w===project.width&&e.h===project.height);els.aspectSel.innerHTML=ASPECT_PRESETS.map((t,n)=>`<option value="${n}" ${n===e?"selected":""}>${uiText(t.label)}</option>`).join("")+(e<0?`<option value="custom" selected>${uiText("Custom · {width}×{height}",{width:project.width,height:project.height})}</option>`:"")}function syncFpsSel(){if(!els.fpsSel)return;const e=Number(project.fps),t=FPS_PRESETS.findIndex(t=>t===e);els.fpsSel.innerHTML=FPS_PRESETS.map((e,n)=>`<option value="${n}" ${n===t?"selected":""}>${e} fps</option>`).join("")+(t<0&&Number.isFinite(e)&&e>0?`<option value="custom" selected>${uiText("Custom · {fps} fps",{fps:e})}</option>`:"")}$("setLinkSelect").addEventListener("change",e=>{setSetting("linkSelect",!!e.target.checked),getSetting("linkSelect")?(selectedMediaIds().size&&"project"!==state.binTab&&setBinTab("project"),syncBinSelectionFromTimeline()):clearBinSelectionHighlight()}),els.btnSnap.addEventListener("click",()=>{state.snap=!state.snap,syncSnapButton()}),syncSnapButton(),els.btnAudioHold&&els.btnAudioHold.addEventListener("click",()=>setAudioHold(!state.audioHold)),$("btnLayoutReset").addEventListener("click",restoreDefaultLayout),$("trackSizeGroup").addEventListener("click",e=>{const t=e.target.closest("[data-track-size]");t&&setTrackSize(t.dataset.trackSize)}),els.binTabs.addEventListener("click",e=>{const t=e.target.closest("[data-tab]");t&&setBinTab(t.dataset.tab)}),els.binTabs.addEventListener("contextmenu",e=>{const t=e.target.closest("[data-tab]");t&&"project"===t.dataset.tab&&(e.preventDefault(),openProjectTabMenu(e.clientX,e.clientY))}),els.aspectSel.addEventListener("change",()=>{const e=ASPECT_PRESETS[+els.aspectSel.value];e&&(project.width=e.w,project.height=e.h,els.preview.width=e.w,els.preview.height=e.h,syncAspectSel(),seekMediaWhilePaused(),scheduleSave())}),els.fpsSel?.addEventListener("change",()=>{const e=FPS_PRESETS[+els.fpsSel.value];e>0&&(project.fps=e,syncFpsSel(),state.dirtyTimeline=!0,seekMediaWhilePaused(),scheduleSave())}),els.btnGuides.addEventListener("click",()=>{state.guides=!state.guides,els.btnGuides.classList.toggle("on",state.guides),els.safeOverlay.classList.toggle("hidden",!state.guides),state.guides&&updateSafeOverlay()});const VIEW_ZOOM_MIN=1,VIEW_PIXEL_MAX=2;let monitorFitCache=null,monitorViewPad={x:0,y:0};function measureMonitorFit(){const e=els.monitorStage.clientWidth,t=els.monitorStage.clientHeight,n=project.width||els.preview.width||1,i=project.height||els.preview.height||1,r=Math.min(e/n,t/i);return{w:n*r,h:i*r}}function monitorFitSize(){if(monitorFitCache)return monitorFitCache;const e=els.preview.offsetWidth,t=els.preview.offsetHeight;return e>0&&t>0&&state.viewZoom<=1.001?{w:e,h:t}:measureMonitorFit()}function maxViewZoom(){const{w:e}=monitorFitSize(),t=project.width||els.preview.width;return e&&t?Math.max(1,2*t/e):1}function applyMonitorView(){const e=state.viewZoom,t=e>1.001,n=els.monitorScroll,i=els.monitorZoomInner;if(t){monitorFitCache||(monitorFitCache=monitorFitSize());const t=monitorFitCache;els.preview.style.width=t.w*e+"px",els.preview.style.height=t.h*e+"px",monitorViewPad.x||monitorViewPad.y||(monitorViewPad={x:Math.ceil(els.monitorStage.clientWidth||n.clientWidth||0),y:Math.ceil(els.monitorStage.clientHeight||n.clientHeight||0)}),i&&(i.style.padding=`${monitorViewPad.y}px ${monitorViewPad.x}px`)}else state.viewZoom=1,monitorFitCache=null,monitorViewPad={x:0,y:0},els.preview.style.width="",els.preview.style.height="",i&&(i.style.padding=""),n.scrollLeft=0,n.scrollTop=0;els.btnZoom100.classList.toggle("hidden",!t),n.classList.toggle("is-zoomed",t),updateSafeOverlay()}let monitorViewRaf=0,monitorViewAfter=null;function scheduleMonitorView(e){e&&(monitorViewAfter=e),monitorViewRaf||(monitorViewRaf=requestAnimationFrame(()=>{monitorViewRaf=0;const e=monitorViewAfter;monitorViewAfter=null,applyMonitorView(),e&&e()}))}function resetMonitorView(){state.viewZoom=1,monitorFitCache=null,monitorViewPad={x:0,y:0},monitorViewAfter=null,monitorViewRaf&&(cancelAnimationFrame(monitorViewRaf),monitorViewRaf=0),applyMonitorView()}els.btnZoom100.addEventListener("click",resetMonitorView);let viewPanDrag=null;function endViewPan(e){if(viewPanDrag){viewPanDrag=null,els.monitorScroll.classList.remove("is-panning");try{els.monitorScroll.releasePointerCapture(e.pointerId)}catch{}}}function updateSafeOverlay(){if(!state.guides)return;const e=els.preview,t=els.safeOverlay.style;t.left=e.offsetLeft+"px",t.top=e.offsetTop+"px",t.width=e.offsetWidth+"px",t.height=e.offsetHeight+"px",els.safeOverlay.classList.toggle("vertical",project.height>project.width)}els.monitorScroll.addEventListener("pointerdown",e=>{if(!(state.viewZoom<=1.001)&&(1===e.button||0===e.button&&e.altKey)){const t=els.monitorScroll;viewPanDrag={x:e.clientX,y:e.clientY,sl:t.scrollLeft,st:t.scrollTop},t.classList.add("is-panning"),t.setPointerCapture(e.pointerId),e.preventDefault()}}),els.monitorScroll.addEventListener("pointermove",e=>{if(!viewPanDrag)return;const t=els.monitorScroll;t.scrollLeft=viewPanDrag.sl-(e.clientX-viewPanDrag.x),t.scrollTop=viewPanDrag.st-(e.clientY-viewPanDrag.y)}),els.monitorScroll.addEventListener("pointerup",endViewPan),els.monitorScroll.addEventListener("pointercancel",endViewPan),els.monitorScroll.addEventListener("auxclick",e=>{1===e.button&&e.preventDefault()}),els.monitorScroll.addEventListener("scroll",()=>{state.guides&&updateSafeOverlay(),scheduleMonitorView()}),"undefined"!=typeof ResizeObserver&&new ResizeObserver(()=>{if(state.viewZoom<=1.001)return monitorFitCache=null,void(state.guides&&updateSafeOverlay());const e=els.monitorScroll,t=e.scrollWidth>0?e.scrollLeft/e.scrollWidth:0,n=e.scrollHeight>0?e.scrollTop/e.scrollHeight:0;monitorFitCache=measureMonitorFit(),state.viewZoom>maxViewZoom()&&(state.viewZoom=maxViewZoom()),applyMonitorView(),e.scrollLeft=t*e.scrollWidth,e.scrollTop=n*e.scrollHeight}).observe(els.monitorStage),window.addEventListener("keydown",e=>{if($("coverDialog").open)return;const t=e.key;"Delete"!==t&&"Backspace"!==t||isTypingTarget(document.activeElement)||!clearFocusedTransition()?isTypingTarget(document.activeElement)||(" "===t?(e.preventDefault(),state.playing?pause():play()):"k"!==t&&"K"!==t||e.ctrlKey||e.metaKey||e.altKey?"l"!==t&&"L"!==t||e.ctrlKey||e.metaKey||e.altKey?"j"!==t&&"J"!==t||e.ctrlKey||e.metaKey||e.altKey?"s"===t||"S"===t?splitAtPlayhead():"g"!==t&&"G"!==t||e.ctrlKey||e.metaKey||e.altKey?!e.altKey||e.ctrlKey||e.metaKey||"t"!==t&&"T"!==t?"t"!==t&&"T"!==t||e.ctrlKey||e.metaKey||e.altKey?"Delete"===t||"Backspace"===t?deleteSelected():!e.ctrlKey&&!e.metaKey||e.altKey||"ArrowLeft"!==t&&"ArrowRight"!==t?"ArrowLeft"===t?setTime(state.time-(e.shiftKey?1:1/project.fps)):"ArrowRight"===t?setTime(state.time+(e.shiftKey?1:1/project.fps)):"Home"===t?gotoHome():"End"===t?gotoEnd():"["===t?trimToPlayhead("in"):"]"===t?trimToPlayhead("out"):"m"===t||"M"===t?toggleMarker():"i"!==t&&"I"!==t||e.ctrlKey||e.metaKey||e.altKey?"o"!==t&&"O"!==t||e.ctrlKey||e.metaKey||e.altKey?"n"===t||"N"===t?els.btnSnap.click():"Escape"===t?$("settingsOverlay").classList.contains("hidden")?$("helpOverlay").classList.contains("hidden")?selectClip(null):(e.preventDefault(),$("helpOverlay").classList.add("hidden")):(e.preventDefault(),closeSettings()):!e.ctrlKey&&!e.metaKey||"a"!==t&&"A"!==t?"+"===t||"="===t?setZoom(1.25*state.pps):"-"===t?setZoom(state.pps/1.25):"KeyZ"!==e.code||e.ctrlKey||e.metaKey?!e.ctrlKey&&!e.metaKey||"z"!==t&&"Z"!==t?!e.ctrlKey&&!e.metaKey||"y"!==t&&"Y"!==t||(e.preventDefault(),redo()):(e.preventDefault(),e.shiftKey?redo():undo()):(e.preventDefault(),e.altKey?zoomToWorkArea():e.shiftKey?zoomToFit():zoomToSelection()):(e.preventDefault(),setSelection(project.clips.map(e=>e.id))):(e.preventDefault(),e.shiftKey?clearOutPoint():setOutPoint()):(e.preventDefault(),e.shiftKey?clearInPoint():setInPoint()):(e.preventDefault(),goToKeyframe("ArrowRight"===t?1:-1)):(e.preventDefault(),e.shiftKey?trimToWorkArea():splitAtWorkArea()):(e.preventDefault(),addTransitionAtPlayhead()):(e.preventDefault(),e.shiftKey?closeGapAtPlayhead():goToNextGap()):(e.preventDefault(),state.playing?stepPreviewRate(-1):play()):(e.preventDefault(),state.playing?stepPreviewRate(1):play()):(e.preventDefault(),setPreviewRate(1),state.playing?pause():play())):e.preventDefault()}),window.addEventListener("resize",()=>{state.dirtyTimeline=!0,clampTimelineHeight()});const TL_H_KEY="fablecut-timeline-h",TL_H_MIN=180,UPPER_MIN=140;function availableTimelineMax(){const e=$("app").getBoundingClientRect(),t=document.querySelector(".topbar")?.getBoundingClientRect(),n=t?t.height:46;return Math.floor(e.height-n-140-6-18)}function setTimelineHeight(e){const t=clamp(Math.round(e),TL_H_MIN,Math.max(TL_H_MIN,availableTimelineMax()));return $("app").style.setProperty("--timeline-h",t+"px"),state.dirtyTimeline=!0,t}function defaultTimelineHeight(){const e=visibleTracks().reduce((e,t)=>e+t.h,0)||64,t=document.querySelector(".timeline-toolbar");return e+26+(t?t.getBoundingClientRect().height:40)+8}function resetTimelineHeight(){const e=setTimelineHeight(defaultTimelineHeight());return localStorage.removeItem(TL_H_KEY),state.dirtyTimeline=!0,e}function trackSizeShowsThumbs(){return!!(TRACK_SIZE_PRESETS[state.trackSize]||TRACK_SIZE_PRESETS.s).thumbs}function syncTrackSizeButtons(){const e=$("trackSizeGroup");if(e){for(const t of e.querySelectorAll("[data-track-size]"))t.classList.toggle("on",t.dataset.trackSize===state.trackSize);document.body.classList.toggle("track-size-s","s"===state.trackSize),document.body.classList.toggle("track-size-m","m"===state.trackSize),document.body.classList.toggle("track-size-l","l"===state.trackSize)}}function applyTrackHeights(){const e=TRACK_SIZE_PRESETS[state.trackSize]||TRACK_SIZE_PRESETS.s;for(const t of TRACKS){const n=e.h[t.id]??e.h["audio"===t.kind?"A1":"V1"];null!=n&&(t.h=n)}}function setTrackSize(e,{persist:t=!0,fitPane:n=!0}={}){if(getSetting("showLayoutControls")&&TRACK_SIZE_PRESETS[e]||(e="l"),state.trackSize=e,applyTrackHeights(),t&&localStorage.setItem(TRACK_SIZE_KEY,e),syncTrackSizeButtons(),buildTrackDOM(),state.dirtyTimeline=!0,rebuildClips(),n){const e=setTimelineHeight(defaultTimelineHeight());localStorage.setItem(TL_H_KEY,String(e))}}function restoreDefaultLayout(){setTrackSize("l",{persist:!0,fitPane:!1}),resetTimelineHeight()}function clampTimelineHeight(){const e=$("timelinePanel")?.getBoundingClientRect().height;e&&setTimelineHeight(e)}function initPanelSplit(){const e=$("splitUpperTimeline"),t=$("timelinePanel");if(!e||!t)return;state.trackSize="l",applyTrackHeights(),syncTrackSizeButtons();const n=parseFloat(localStorage.getItem(TL_H_KEY));n>0?setTimelineHeight(n):resetTimelineHeight(),e.addEventListener("pointerdown",n=>{if(0!==n.button)return;n.preventDefault(),e.setPointerCapture(n.pointerId),e.classList.add("dragging"),document.body.classList.add("resizing-panels");const i=n.clientY,r=t.getBoundingClientRect().height,a=e=>setTimelineHeight(r-(e.clientY-i)),o=()=>{e.releasePointerCapture(n.pointerId),e.classList.remove("dragging"),document.body.classList.remove("resizing-panels"),e.removeEventListener("pointermove",a),e.removeEventListener("pointerup",o),localStorage.setItem(TL_H_KEY,String(Math.round(t.getBoundingClientRect().height))),state.dirtyTimeline=!0};e.addEventListener("pointermove",a),e.addEventListener("pointerup",o)}),e.addEventListener("dblclick",e=>{e.preventDefault(),resetTimelineHeight()})}loadSettings(),applyInterfaceVisibility(),updateProjectTitle(),FableCutI18n.setLanguage(getSetting("language")),FableCutI18n.apply(document),$("setLanguage").value=getSetting("language"),syncAspectSel(),syncFpsSel(),initPanelSplit(),buildTrackDOM(),rebuildClips(),renderBin(),syncTrimIOButton(),buildMeterDOM(),connectServer().then(async()=>{await loadLibraryFonts();const e=new URLSearchParams(location.search),t=e.get("cliExport"),n=e.get("exportJob")||t;if(/^[a-f0-9]{32}$/.test(n||"")&&installExportPageGuards(n),state.connected&&/^[a-f0-9]{32}$/.test(t||"")){await new Promise(e=>requestAnimationFrame(()=>requestAnimationFrame(e)));const n=e.get("cliExportName")||void 0;"realtime"===e.get("cliExportEngine")&&e.get("exportJob")===t?await runRealtimeJob(t,n):await runExportPlan(t,n,e.get("cliExportEngine"))}else await restoreExportDialog()}),requestAnimationFrame(loop);
|
|
1
|
+
"use strict";const uiText=(e,t)=>FableCutI18n.text(e,t),uiAttr=(e,t)=>escapeHtml(uiText(e,t)),Timeline=FableCutTimeline,TRACKS=[],TRACK_SIZE_PRESETS={s:{thumbs:!1,h:{V3:26,V2:26,V1:26,A1:22,A2:22,A3:22,A4:22}},m:{thumbs:!0,h:{V3:36,V2:44,V1:44,A1:32,A2:32,A3:32,A4:32}},l:{thumbs:!0,h:{V3:44,V2:58,V1:58,A1:42,A2:42,A3:42,A4:42}}},MAX_SOURCE_CHANNELS=16,TRACK_SIZE_KEY="fablecut-track-size",LAST_TRANS_KEY={in:"fablecut-last-trans-in",out:"fablecut-last-trans-out"},DEFAULT_LAST_TRANS={type:"fade",duration:1},RULER_H=26,SNAP_PX=8,MIN_DUR=.05,ZOOM_MIN=1,ZOOM_MAX=300,TIMELINE_FIT_FILL=.95,PROJECT_KEY="fablecut-project",requestedProjectId=new URLSearchParams(location.search).get("project_id");let activeProjectId=(requestedProjectId??localStorage.getItem(PROJECT_KEY)??"default").trim().toLowerCase();function projectApi(e){const t=new URLSearchParams(location.search).get("exportJob");return e+(e.includes("?")?"&":"?")+"project="+encodeURIComponent(activeProjectId)+(t?"&exportJob="+encodeURIComponent(t):"")}null!==requestedProjectId||/^[a-z0-9][a-z0-9_-]{0,63}$/.test(activeProjectId)||(activeProjectId="default");const DEFAULT_PROPS={x:0,y:0,scale:1,rotation:0,opacity:1,volume:1,speed:1,brightness:100,contrast:100,saturation:100,hue:0,blur:0,grayscale:0,sepia:0,invert:0,temperature:0,tint:0,vignette:0,filterPreset:"none",fit:"contain",cropL:0,cropT:0,cropR:0,cropB:0,flipH:!1,flipV:!1,cornerRadius:0,blend:"normal",chromaKey:"",chromaTolerance:26,chromaSoftness:12,bgRemove:!1,shake:0,shakeSpeed:8,rgbSplit:0,grain:0,text:"Enter Text",fontSize:72,color:"#ffffff",color2:"",font:"Segoe UI",bold:!0,italic:!1,weight:0,align:"center",letterSpacing:0,lineHeight:1.2,uppercase:!1,textShadow:12,glow:0,glowColor:"",textAnim:"none",wordRate:.15,direction:"auto",strokeWidth:0,strokeColor:"#000000",bgColor:"#000000",bgOpacity:0,boxW:0,boxH:0,boxFit:!1,vAlign:"middle"},ANIMATABLE=["x","y","scale","rotation","opacity","volume","speed","brightness","contrast","saturation","hue","blur","grayscale","sepia","invert","temperature","tint","vignette","cornerRadius","shake","rgbSplit","grain","fontSize","letterSpacing","glow"],TRANSITIONS=["none","fade","slide-left","slide-right","slide-up","slide-down","zoom","wipe","wipe-right","wipe-up","wipe-down","iris","spin","blur","whip","glitch","pop"],TEXT_ANIMS=["none","typewriter","word-pop","word-slide","karaoke","letter-pop","wave","bounce","shake","clip-reveal","zoom-in","font-cut","rise-mask"],BLEND_MODES=["normal","multiply","screen","overlay","lighter","soft-light","hard-light","color-dodge","darken","lighten","difference"],FILTER_PRESETS={none:{},cinematic:{contrast:112,saturation:118,temperature:-10,vignette:28},"teal-orange":{contrast:115,saturation:125,temperature:-18,hue:-8,vignette:22},noir:{grayscale:100,contrast:128,brightness:96,vignette:45},vintage:{sepia:42,contrast:92,brightness:106,saturation:88,temperature:12,vignette:30},faded:{contrast:84,brightness:110,saturation:82},warm:{temperature:28,brightness:103,saturation:108},cold:{temperature:-28,saturation:104},pop:{saturation:152,contrast:116},dreamy:{brightness:109,saturation:112,blur:.6,temperature:8},retro:{saturation:130,hue:-6,contrast:106,sepia:15},"bw-soft":{grayscale:100,contrast:95,brightness:108},cyberpunk:{saturation:140,hue:12,contrast:118,temperature:-15,vignette:25},sunset:{temperature:24,tint:6,brightness:105,saturation:116,contrast:104,vignette:20},midnight:{temperature:-24,brightness:88,contrast:122,saturation:94,vignette:38}},SYSTEM_FONTS=["Segoe UI","Arial","Georgia","Impact","Courier New","Trebuchet MS","Verdana","Times New Roman","Comic Sans MS","Consolas"],FONT_CUT_DEFAULT=["SourceHanSansSC-Bold","Rubik-Black","阿里巴巴普惠体-粗体","阿里巴巴普惠体-中黑","Impact"],STYLE_RESET={color2:"",glow:0,glowColor:"",strokeWidth:0,bgColor:"#000000",bgOpacity:0,rotation:0,letterSpacing:0,uppercase:!1,italic:!1,textShadow:12,fontCutSet:void 0,align:"center"},TITLE_STYLES={plain:{label:"Plain",place:"center",props:{font:"Segoe UI",fontSize:72,bold:!0,color:"#ffffff",textAnim:"none"}},impact:{label:"Impact",place:"lower",props:{font:"SourceHanSansSC-Bold",fontSize:96,bold:!1,uppercase:!0,color:"#ffffff",textShadow:22,textAnim:"word-pop"}},elegant:{label:"Elegant",place:"center",props:{font:"Georgia",fontSize:88,bold:!1,color:"#ffffff",color2:"#ffd166",letterSpacing:2,textAnim:"clip-reveal"}},kinetic:{label:"Kinetic cut",place:"center",props:{font:"Rubik-Black",fontSize:120,bold:!1,uppercase:!0,color:"#ffd166",letterSpacing:3,textAnim:"font-cut",fontCutSet:["SourceHanSansSC-Bold","Rubik-Black","阿里巴巴普惠体-粗体","阿里巴巴普惠体-中黑"]}},neon:{label:"Neon",place:"center",props:{font:"阿里巴巴普惠体-中黑",fontSize:104,bold:!1,uppercase:!0,color:"#ffffff",glow:60,glowColor:"#22d3ee",textAnim:"wave"}},handwritten:{label:"Handwritten",place:"lower-left",props:{font:"Comic Sans MS",fontSize:92,bold:!1,color:"#ffffff",rotation:-4,textAnim:"word-slide"}},serifDrop:{label:"Serif drop",place:"center",props:{font:"Times New Roman",fontSize:96,bold:!1,color:"#ffffff",textShadow:18,textAnim:"zoom-in"}},subtitle:{label:"Subtitle",place:"lower",props:{font:"Roboto-Regular",fontSize:52,bold:!1,color:"#ffffff",bgColor:"#000000",bgOpacity:.5,textAnim:"karaoke"}},boldRise:{label:"Bold rise",place:"lower",props:{font:"阿里巴巴普惠体-粗体",fontSize:92,bold:!1,uppercase:!0,color:"#ffffff",textAnim:"rise-mask"}},luxury:{label:"Luxury",place:"center",props:{font:"Chocolate Classical Sans",fontSize:88,bold:!1,uppercase:!0,color:"#faf0dc",color2:"#c9a227",letterSpacing:6,textAnim:"clip-reveal"}}},STYLE_CYCLE=["impact","elegant","kinetic","neon","handwritten","serifDrop","boldRise","luxury"],AUDIO_EXT=/\.(mp3|wav|ogg|m4a|aac|flac|mpeg)$/i,VIDEO_EXT=/\.(mp4|webm|mov|mkv|m4v)$/i,IMAGE_EXT=/\.(png|jpe?g|gif|webp|avif)$/i,ASPECT_PRESETS=[{label:"16:9 · 1280×720",w:1280,h:720},{label:"16:9 · 1920×1080",w:1920,h:1080},{label:"9:16 · Reel 1080×1920",w:1080,h:1920},{label:"4:5 · IG 1080×1350",w:1080,h:1350},{label:"1:1 · 1080×1080",w:1080,h:1080}],FPS_PRESETS=[24,25,30,50,60],WAVE_PEAKS_PER_SEC=50,TRACK_IDS=new Set(TRACKS.map(e=>e.id)),AUDIO_TRACK_IDS=[];function ensureTrack(e){if(!project.tracks.some(t=>t.id===e)){const t="A"===e[0]?"audio":"video";let n=project.tracks.findIndex(n=>"video"===t&&("audio"===n.kind||+n.id.slice(1)<+e.slice(1)));n<0&&(n=project.tracks.length),project.tracks.splice(n,0,{id:e,kind:t})}return refreshTrackRegistry(),e}function ensureAudioTrackCount(e){const t=AUDIO_TRACK_IDS.length;for(let t=1;t<=Math.min(e,16);t++)ensureTrack("A"+t);return AUDIO_TRACK_IDS.length-t}function visibleTracks(){const e=new Set(project.clips.map(e=>e.track));return TRACKS.filter(t=>e.has(t.id))}function visibleTrackSignature(){return visibleTracks().map(e=>`${e.id}:${e.h}:${e.name||""}`).join(",")}function refreshTrackRegistry(){const e={video:["#4f8cff","#7b6cff","#ffd166"],audio:["#7ec249","#5a9e3a","#4a8a2f","#3a7226"]},t=TRACK_SIZE_PRESETS[state.trackSize]||TRACK_SIZE_PRESETS.s;TRACKS.splice(0,TRACKS.length,...(project.tracks||[]).map(n=>({...n,h:t.h[n.id]??t.h["audio"===n.kind?"A1":"V1"],color:e[n.kind][(+n.id.slice(1)-1)%e[n.kind].length]}))),TRACK_IDS.clear();for(const e of TRACKS)TRACK_IDS.add(e.id);if(AUDIO_TRACK_IDS.splice(0,AUDIO_TRACK_IDS.length,...TRACKS.filter(e=>"audio"===e.kind).map(e=>e.id)),runtime.audio){const e=runtime.audio,t=visibleTracks().filter(e=>"audio"===e.kind).map(e=>e.id),n=Object.keys(e.trackBus).filter(e=>!t.includes(e));for(const t of n)e.trackBus[t].disconnect(),delete e.trackBus[t];const i=t.filter(t=>!e.trackBus[t]);e.audioTrackIds=Object.keys(e.trackBus),(i.length||n.length)&&addLiveAudioTrackBuses(i)}}function syncTimeline(){Timeline.normalize(project),refreshTrackRegistry(),state.disabledTracks=new Set(project.disabledTracks||[]);const e=visibleTrackSignature();els.tracks.dataset.signature!==e&&(buildTrackDOM(),buildMeterDOM(),localStorage.getItem(TL_H_KEY)||setTimelineHeight(defaultTimelineHeight())),syncAllTrackDisabledUI();for(const e of project.clips)routeClipGain(e)}function timelineSnapshot(){return JSON.stringify({clips:project.clips,tracks:project.tracks,disabledTracks:project.disabledTracks,cover:project.cover})}function restoreTimeline(e){const t=JSON.parse(e),n=Array.isArray(t)?{clips:t}:t,i=new Map(project.clips.map(e=>[e.id,e]));n.clips=n.clips.map(e=>{const t=i.get(e.id);if(!t)return e;for(const n of Object.keys(t))Object.hasOwn(e,n)||delete t[n];return Object.assign(t,e)}),Object.assign(project,n)}function addLiveAudioTrackBuses(e){const t=runtime.audio;if(t){for(const n of e)t.trackBus[n]||(t.trackBus[n]=t.ctx.createGain());t.audioTrackIds=Object.keys(t.trackBus),t.meterVersion=(t.meterVersion||0)+1;for(const e of Object.values(t.trackBus))e.disconnect(),e.connect(t.master);for(const e of t.meters||[])e.port.onmessage=null,e.disconnect();t.meters=[],t.meterReady=!1,installMeterWorklet(t).catch(()=>{})}}const SETTINGS_KEY="fablecut-settings",DEFAULT_UI_VISIBILITY={showAssets:!1,showInspector:!1,showNewProject:!1,showConnectionStatus:!1,showSettings:!1,showCloseGap:!1,showFindGap:!1,showSnap:!1,showAudioHold:!1,showProjectSwitcher:!1,showLayoutControls:!1,showShortcuts:!1,showMonitorHeader:!1,showAudioMeter:!1,showBoundaryNavigation:!1,showTimelineFit:!0,showCover:!1},DEFAULT_SETTINGS={language:"zh-CN",exportEngine:"webcodecs",exportProfile:"delivery",linkSelect:!1,...DEFAULT_UI_VISIBILITY};let settings={...DEFAULT_SETTINGS};function loadSettings(){try{const e=JSON.parse(localStorage.getItem(SETTINGS_KEY)||"null");if(!e||"object"!=typeof e)return void(settings={...DEFAULT_SETTINGS});const t={...DEFAULT_SETTINGS};for(const n of Object.keys(DEFAULT_SETTINGS))Object.hasOwn(e,n)&&(t[n]=e[n]);t.language=FableCutI18n.normalize(t.language),t.linkSelect=!!t.linkSelect,["optimized","fast","webcodecs"].includes(t.exportEngine)||(t.exportEngine="webcodecs"),["delivery","auto","videotoolbox-h264","videotoolbox-hevc","nvenc-h264","nvenc-hevc"].includes(t.exportProfile)||(t.exportProfile="delivery");for(const e of Object.keys(DEFAULT_UI_VISIBILITY))"boolean"!=typeof t[e]&&(t[e]=DEFAULT_UI_VISIBILITY[e]);settings=t}catch{settings={...DEFAULT_SETTINGS}}}function saveSettings(){try{localStorage.setItem(SETTINGS_KEY,JSON.stringify(settings))}catch{}}function getSetting(e){return settings[e]}function setSetting(e,t){if(!Object.hasOwn(DEFAULT_SETTINGS,e))return;if("exportEngine"===e&&!["optimized","fast","webcodecs"].includes(t))return;const n=Object.hasOwn(DEFAULT_UI_VISIBILITY,e);n&&"boolean"!=typeof t||(settings[e]=t,saveSettings(),n&&applyInterfaceVisibility(),"showLayoutControls"!==e||t||setTrackSize("l"),"showCover"===e&&(!t&&$("coverDialog").open&&$("coverDialog").close(),refreshCoverButton()))}function applyInterfaceVisibility(){for(const e of document.querySelectorAll("[data-ui-setting]"))e.toggleAttribute("data-ui-visible",!0===getSetting(e.dataset.uiSetting));const e=[];getSetting("showAssets")&&e.push("minmax(0, var(--side-panel-w))"),e.push("minmax(0, 1fr)"),getSetting("showInspector")&&e.push("minmax(0, var(--side-panel-w))"),document.querySelector(".upper").style.gridTemplateColumns=e.join(" ");for(const e of document.querySelectorAll("[data-visibility-setting]"))e.checked=!0===getSetting(e.dataset.visibilitySetting)}const project={name:"Untitled Project",width:1280,height:720,fps:30,background:"#000000",revision:0,cover:null,folders:[],media:[],tracks:[],clips:[],markers:[],inPoint:null,outPoint:null,disabledTracks:[]},state={time:0,playing:!1,pps:60,snap:!0,previewRate:1,selId:null,selIds:new Set,trackSize:"l",connected:!1,exporting:!1,rendering:!1,guides:!1,viewZoom:1,audioHold:!1,ffmpeg:!1,ffprobe:!1,dirtyTimeline:!0,gesture:!1,clipGestureWidth:null,clipTrimOrigins:null,workAreaPlay:!1,binTab:"project",disabledTracks:new Set,transFocus:null,kfGraphs:new Set};function normalizeDisabledTracks(e){if(null==e)return[];const t=Array.isArray(e)?e:[];return[...new Set(t.filter(e=>TRACK_IDS.has(e)))].sort()}function isTrackEnabled(e){return!state.disabledTracks.has(e)}function syncTrackDisabledUI(e){const t=isTrackEnabled(e),n=els.trackHeaders.querySelector(`.track-head[data-track="${e}"]`),i=els.tracks.querySelector(`.track[data-track="${e}"]`);if(n){n.classList.toggle("disabled",!t),n.querySelector(".track-id").textContent=trackLabel(e);const i=n.querySelector(".track-toggle");i&&(i.setAttribute("aria-pressed",t?"true":"false"),i.title=`${trackLabel(e)} · ${uiText(t?"Disable track":"Enable track")}`,i.setAttribute("aria-label",i.title),i.innerHTML=trackToggleIcon(trackIconKind(e)))}i&&i.classList.toggle("disabled",!t)}function syncAllTrackDisabledUI(){for(const e of TRACKS)syncTrackDisabledUI(e.id)}function toggleTrackEnabled(e){TRACK_IDS.has(e)&&(state.disabledTracks.has(e)?state.disabledTracks.delete(e):state.disabledTracks.add(e),project.disabledTracks=[...state.disabledTracks].sort(),syncTrackDisabledUI(e),scheduleSave())}const runtime={clipEls:new Map,previewMedia:new Map,previewWait:null,previewRemainder:0,scrub:null,clipGain:new Map,mediaAux:new Map,audioBufs:new Map,wavePeaks:new Map,library:{},customFonts:[],fontsLoading:null,undo:[],redo:[],audio:null,saveTimer:null,savePromise:null,pendingSync:!1,sfxPreview:null,importFolderId:null,binDragFolderId:null,binCtxMenu:null},$=e=>document.getElementById(e),els={binList:$("binList"),binEmpty:$("binEmpty"),fileInput:$("fileInput"),binTabs:$("binTabs"),libList:$("libList"),toast:$("toast"),preview:$("preview"),tcCurrent:$("tcCurrent"),tcTotal:$("tcTotal"),btnPlay:$("btnPlay"),inspector:$("inspector"),trackHeaders:$("trackHeaders"),timelineScroll:$("timelineScroll"),tracksContent:$("tracksContent"),tracks:$("tracks"),playhead:$("playhead"),coverFrames:$("coverFrames"),ruler:$("ruler"),zoomSlider:$("zoomSlider"),btnSnap:$("btnSnap"),thumbProgress:$("thumbProgress"),thumbProgressBar:$("thumbProgressBar"),thumbProgressLabel:$("thumbProgressLabel"),btnAudioHold:$("btnAudioHold"),exportOverlay:$("exportOverlay"),exportProgress:$("exportProgress"),exportTitle:$("exportTitle"),exportNote:$("exportNote"),projectName:$("projectName"),projectSelect:$("projectSelect"),btnNewProject:$("btnNewProject"),currentProjectName:$("currentProjectName"),aspectSel:$("aspectSel"),fpsSel:$("fpsSel"),btnGuides:$("btnGuides"),btnZoom100:$("btnZoom100"),safeOverlay:$("safeOverlay"),btnSpeed:$("btnSpeed"),monitorStage:$("monitorStage"),monitorScroll:$("monitorScroll"),monitorZoomInner:$("monitorZoomInner"),kfGraphs:$("kfGraphs")},ctx2d=els.preview.getContext("2d"),uid=()=>Math.random().toString(36).slice(2,9),clamp=(e,t,n)=>Math.min(n,Math.max(t,e));function escapeHtml(e){return String(e??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}function fmt(e){e=Math.max(0,e);const t=Math.floor(e/60),n=Math.floor(e%60),i=Math.floor(e%1*project.fps),r=e=>String(e).padStart(2,"0");return`${r(t)}:${r(n)}:${r(i)}`}const getMedia=e=>project.media.find(t=>t.id===e),getClip=e=>project.clips.find(t=>t.id===e),clipEnd=e=>e.start+e.duration,trackOf=e=>TRACKS.find(t=>t.id===e.track);function projDur(){let e=0;for(const t of project.clips){if(!trackOf(t))continue;const n=clipEnd(t);Number.isFinite(n)&&n>e&&(e=n)}return e}const clipSpeed=e=>clamp(+e.props?.speed||1,.1,8),speedIntCache=new Map;function kfChannel(e,t,n,i){const r=e.keyframes?.[t];if(!Array.isArray(r)||!r.length)return i;if(n<=r[0].t)return r[0].v;if(n>=r[r.length-1].t)return r[r.length-1].v;for(let e=0;e<r.length-1;e++){const t=r[e],i=r[e+1];if(n>=t.t&&n<=i.t){const e=(n-t.t)/Math.max(1e-6,i.t-t.t),r=EASE[i.ease||"ease-in-out"]||EASE.linear;return t.v+(i.v-t.v)*r(e)}}return i}function hasSpeedRamp(e){return Array.isArray(e.keyframes?.speed)&&e.keyframes.speed.length>0}function mediaTimeAt(e,t){const n=clipSpeed(e),i=clamp(t-e.start,0,e.duration);if(!hasSpeedRamp(e))return e.in+i*n;const r=JSON.stringify(e.keyframes.speed)+"|"+e.duration.toFixed(4)+"|"+n;let a=speedIntCache.get(e.id);if(!a||a.key!==r){const t=1/120,i=Math.max(2,Math.ceil(e.duration/t)+1),o=new Float32Array(i);let s=clamp(kfChannel(e,"speed",0,n),.1,8);for(let r=1;r<i;r++){const i=Math.min(e.duration,r*t),a=clamp(kfChannel(e,"speed",i,n),.1,8);o[r]=o[r-1]+(s+a)/2*(i-(r-1)*t),s=a}a={key:r,cum:o,step:t},speedIntCache.set(e.id,a)}const o=Math.min(a.cum.length-1,i/a.step),s=Math.floor(o),l=o-s,c=s>=a.cum.length-1?a.cum[a.cum.length-1]:a.cum[s]+(a.cum[s+1]-a.cum[s])*l;return e.in+c}let toastTimer=null;function toast(e){els.toast&&(els.toast.textContent=e,els.toast.classList.add("show"),clearTimeout(toastTimer),toastTimer=setTimeout(()=>els.toast.classList.remove("show"),3200))}function isTypingTarget(e){if(!e||e===document.body||e===document.documentElement)return!1;const t=e.tagName;if("TEXTAREA"===t||"SELECT"===t)return!0;if("INPUT"===t){const t=(e.type||"text").toLowerCase();return!["range","checkbox","radio","button","submit","reset","color","file","hidden"].includes(t)}return!!e.isContentEditable}function updateProjectTitle(e=project.name){els.currentProjectName.textContent=e,els.currentProjectName.title=e}async function loadProjectCatalog(){const e=await fetch("/api/projects",{cache:"no-store",signal:AbortSignal.timeout(1e4)});if(!e.ok)throw new Error("projects unavailable");const t=await e.json();null!==requestedProjectId||runtime.projectLoaded||!t.length||t.some(e=>e.id===activeProjectId)||(activeProjectId=t.find(e=>"default"===e.id)?.id||t[0].id),localStorage.setItem(PROJECT_KEY,activeProjectId);const n=new URL(location.href);n.searchParams.set("project_id",activeProjectId),history.replaceState(null,"",n),els.projectSelect.replaceChildren(...t.map(e=>{const t=document.createElement("option");return t.value=e.id,t.textContent=e.name,t.selected=e.id===activeProjectId,t})),updateProjectTitle(t.find(e=>e.id===activeProjectId)?.name||project.name)}function openProject(e){localStorage.setItem(PROJECT_KEY,e);const t=new URL(location.href);t.searchParams.set("project_id",e),location.href=t.toString()}async function connectServer(){if(runtime.connecting)return;runtime.connecting=!0;const e=runtime.projectReadVersion=(runtime.projectReadVersion||0)+1;try{if(!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(activeProjectId))throw new Error(uiText("Invalid project link. Open the project again from its original entry."));await loadProjectCatalog();const t=await fetchProjectDocument();if(e!==runtime.projectReadVersion)return;applyProject(t),clearProjectLoadError(),new URLSearchParams(location.search).has("exportJob")||listenSSE(),fetch("/api/export/ffmpeg").then(e=>e.json()).then(e=>{state.ffmpeg=!!e.available,state.ffprobe=!!e.ffprobe}).catch(()=>{})}catch(t){if(e!==runtime.projectReadVersion)return;state.connected=!1,els.projectName.textContent=uiText("⚪ local session"),"file:"===location.protocol&&null===requestedProjectId||showProjectLoadError(t)}finally{runtime.connecting=!1}state.connected&&!new URLSearchParams(location.search).has("exportJob")&&await probeMissingMeta()}async function fetchProjectDocument(){const e=await fetch(projectApi("/api/project"),{cache:"no-store",signal:AbortSignal.timeout(1e4)});if(!e.ok){const t=new Error(uiText("Project request failed ({status})",{status:e.status}));throw t.missing=404===e.status,t}const t=await e.json();if(!t||!Array.isArray(t.clips)||!Array.isArray(t.media))throw new Error(uiText("Invalid project data"));const n=e=>e&&"object"==typeof e&&!Array.isArray(e);if(t.media.some(e=>!n(e)||"string"!=typeof e.id||!["video","audio","image","svg"].includes(e.kind)||null!=e.name&&"string"!=typeof e.name||null!=e.src&&"string"!=typeof e.src)||t.clips.some(e=>!n(e)||"string"!=typeof e.id||null!=e.props&&!n(e.props)||"text"===e.kind&&null!=e.props?.text&&"string"!=typeof e.props.text)||["markers","folders","disabledTracks"].some(e=>null!=t[e]&&!Array.isArray(t[e])))throw new Error(uiText("Invalid project data"));return t}function showProjectLoadError(e){runtime.projectEpoch=(runtime.projectEpoch||0)+1,runtime.projectLoadError=e,state.connected=!1,clearTimeout(runtime.saveTimer),runtime.saveTimer=null,pause();const t=uiText(e.missing?"Project not found":"Project could not be loaded");$("projectLoadTitle").textContent=t,$("projectLoadHelp").textContent=uiText(e.missing?"This project is not in the current or legacy storage folder. It may have been moved or deleted. Restore the project folder, then retry.":"The project file could not be read or the editor service is unavailable. Check the file or service, then retry."),$("projectLoadId").textContent=uiText("Project ID: {id}",{id:activeProjectId}),$("projectLoadDetails").textContent=e.message||"",runtime.projectLoaded||updateProjectTitle(t);const n=$("projectLoadDialog");n.open||n.showModal()}function clearProjectLoadError(){runtime.projectLoadError=null,runtime.projectLoaded=!0,state.connected=!0,els.projectName.textContent=uiText("🟢 connected"),$("projectLoadDialog").close()}els.projectSelect.addEventListener("change",()=>openProject(els.projectSelect.value)),els.btnNewProject.addEventListener("click",async()=>{const e=prompt(uiText("Project name"),uiText("Untitled Project"));if(null!=e&&e.trim())try{const t=await fetch("/api/projects",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:e.trim()})}),n=await t.json();if(!t.ok)throw new Error(n.error||"create failed");openProject(n.id)}catch(e){alert(uiText("Couldn't create project: ")+e.message)}}),$("projectLoadDialog").addEventListener("cancel",e=>e.preventDefault()),$("projectLoadDialog").addEventListener("keydown",e=>e.stopPropagation()),$("btnRetryProject").addEventListener("click",async()=>{const e=$("btnRetryProject");e.disabled=!0;try{await connectServer()}finally{e.disabled=!1}});const TIMELINE_START_TIME=0;function normalizeWorkArea(e,t,n=0){let i=null!=e&&isFinite(e)?Math.max(n,+e):null,r=null!=t&&isFinite(t)?Math.max(n,+t):null;return null!=i&&null!=r&&r<=i&&(i=null,r=null),{inPoint:i,outPoint:r}}function getFolder(e){return e?project.folders.find(t=>t.id===e):null}function normalizeFolders(e){if(!Array.isArray(e))return[];const t=[],n=new Set;for(const i of e)i&&i.id&&!n.has(i.id)&&(n.add(i.id),t.push({id:String(i.id),name:String(i.name||"Folder").trim()||"Folder",parentId:i.parentId||null,open:!1!==i.open}));for(const e of t)e.parentId&&!n.has(e.parentId)&&(e.parentId=null);return t}function normalizeMediaEntry(e){return e&&"object"==typeof e?{...e,name:e.name||e.id,folderId:e.folderId||null}:e}function folderChildren(e){return project.folders.filter(t=>(t.parentId||null)===(e||null)).sort((e,t)=>e.name.localeCompare(t.name,void 0,{sensitivity:"base"}))}function mediaInFolder(e){return project.media.filter(t=>(t.folderId||null)===(e||null)).sort((e,t)=>e.name.localeCompare(t.name,void 0,{sensitivity:"base"}))}function isSelfOrFolderAncestor(e,t){let n=getFolder(t);for(;n;){if(n.id===e)return!0;n=n.parentId?getFolder(n.parentId):null}return!1}function addFolder(e=null){e&&!getFolder(e)&&(e=null);const t={id:"f_"+uid(),name:"New folder",parentId:e||null,open:!0};if(project.folders.push(t),e){const t=getFolder(e);t&&(t.open=!0)}return scheduleSave(),renderBin(),requestAnimationFrame(()=>startFolderRename(t.id)),t}function deleteFolder(e){const t=getFolder(e);if(!t)return;const n=t.parentId||null,i=new Set,r=e=>{i.add(e);for(const t of project.folders)t.parentId===e&&r(t.id)};r(e);for(const e of project.media)e.folderId&&i.has(e.folderId)&&(e.folderId=n);for(const e of project.folders)e.parentId&&i.has(e.parentId)&&!i.has(e.id)&&(e.parentId=n);project.folders=project.folders.filter(e=>!i.has(e.id)),scheduleSave(),renderBin()}function moveMediaToFolder(e,t){const n=getMedia(e);if(n){if(t&&!getFolder(t)&&(t=null),n.folderId=t||null,t){const e=getFolder(t);e&&(e.open=!0)}scheduleSave(),renderBin()}}function moveFolderToParent(e,t){const n=getFolder(e);if(n&&(!t||t!==e&&!isSelfOrFolderAncestor(e,t))){if(t&&!getFolder(t)&&(t=null),n.parentId=t||null,t){const e=getFolder(t);e&&(e.open=!0)}scheduleSave(),renderBin()}}function startFolderRename(e){const t=els.binList.querySelector(`.bin-folder[data-folder-id="${e}"] .bin-folder-name`);if(!t)return;t.contentEditable="true",t.focus();const n=window.getSelection(),i=document.createRange();i.selectNodeContents(t),n.removeAllRanges(),n.addRange(i),t.addEventListener("blur",()=>{t.contentEditable="false";const n=getFolder(e);if(!n)return;const i=t.textContent.replace(/\s+/g," ").trim()||"Folder";n.name=i,t.textContent=i,scheduleSave()},{once:!0}),t.addEventListener("keydown",n=>{"Enter"===n.key&&(n.preventDefault(),t.blur()),"Escape"===n.key&&(n.preventDefault(),t.textContent=getFolder(e)?.name||"Folder",t.blur())})}function applyProject(e){$("coverDialog").open&&$("coverDialog").close(),runtime.projectEpoch=(runtime.projectEpoch||0)+1,runtime.timelineMenu&&closeTimelineMenu();const t=normalizeWorkArea((e=Timeline.normalize(structuredClone(e))).inPoint,e.outPoint),n=e.disabledTracks||[];Object.assign(project,{name:e.name||"Untitled Project",width:e.width||1280,height:e.height||720,fps:e.fps||30,background:e.background||"#000000",revision:e.revision||0,cover:e.cover||null,delivery:e.delivery||null,outputs:e.outputs||[],folders:normalizeFolders(e.folders),media:(e.media||[]).map(normalizeMediaEntry),tracks:e.tracks||[],clips:e.clips||[],markers:(e.markers||[]).filter(e=>e&&isFinite(e.t)).sort((e,t)=>e.t-t.t),inPoint:t.inPoint,outPoint:t.outPoint,disabledTracks:n});const i=[...els.projectSelect.options].find(e=>e.value===activeProjectId);i&&(i.textContent=project.name),updateProjectTitle();const r=new Set(project.folders.map(e=>e.id));for(const e of project.media)e.folderId&&!r.has(e.folderId)&&(e.folderId=null);state.disabledTracks=new Set(n);for(const e of project.clips){if(e.props={...DEFAULT_PROPS,...e.props||{}},"string"!=typeof e.name){const t=getMedia(e.mediaId)?.name;e.name=t?t.replace(/\.[^.]+$/,""):"text"===e.kind?"Text":"adjust"===e.kind?"Adjustment":"Clip"}if(e.keyframes)for(const t of Object.values(e.keyframes))Array.isArray(t)&&t.sort((e,t)=>e.t-t.t)}syncTimeline(),relinkClips(),state.audioHold?setAudioHold(!1):stopAudioHoldNodes(),resetPreviewWait();for(const e of[...runtime.clipEls.keys()])releaseClipEl(e);els.preview.width=project.width,els.preview.height=project.height,syncAspectSel(),syncFpsSel(),pruneSelection(),state.dirtyTimeline=!0,renderBin(),renderInspector(),updateWorkArea(),syncTrimIOButton(),syncAllTrackDisabledUI(),refreshCoverButton()}function scheduleSave(){runtime.projectLoadError||(refreshCoverButton(),runtime.timelineMenu&&closeTimelineMenu(),state.gesture||syncTimeline(),state.dirtyTimeline=!0,syncHistoryButtons(),state.connected&&(clearTimeout(runtime.saveTimer),runtime.saveTimer=setTimeout(()=>{runtime.saveTimer=null,saveProjectNow().catch(e=>toast(e.message))},400)))}async function saveProjectNow(){if(runtime.projectLoadError)throw new Error(uiText("Project could not be loaded"));for(;runtime.savePromise||state.gesture;)runtime.savePromise?await runtime.savePromise:await new Promise(e=>window.addEventListener("timelinegestureend",e,{once:!0}));if(runtime.projectLoadError)throw new Error(uiText("Project could not be loaded"));project.revision++;const e=JSON.stringify(projectJSON(),null,2),t=(async()=>{const t=await fetch(projectApi("/api/project"),{method:"PUT",headers:{"Content-Type":"application/json"},body:e});if(409===t.status)throw await syncFromServer(!0),new Error(uiText("Project was updated externally — your last change may need redoing."));if(!t.ok)throw new Error("Project save failed")})();runtime.savePromise=t;try{await t}finally{runtime.savePromise===t&&(runtime.savePromise=null)}}function projectJSON(){const{name:e,width:t,height:n,fps:i,background:r,revision:a,folders:o,media:s,clips:l,markers:c,inPoint:d,outPoint:u,disabledTracks:p}=project;return{name:e,width:t,height:n,fps:i,background:r,revision:a,cover:project.cover,...project.delivery?{delivery:project.delivery}:{},...project.outputs?.length?{outputs:project.outputs}:{},tracks:project.tracks.map(({id:e,kind:t})=>({id:e,kind:t})),folders:(o||[]).map(({id:e,name:t,parentId:n,open:i})=>({id:e,name:t,parentId:n||null,open:!1!==i})),media:s.filter(e=>!e.transient).map(({id:e,name:t,kind:n,src:i,duration:r,width:a,height:o,folderId:s,asrUrl:l,asrLocalPath:c})=>({id:e,name:t,kind:n,src:i,duration:r,width:a,height:o,folderId:s||null,...void 0===l?{}:{asrUrl:l},...void 0===c?{}:{asrLocalPath:c}})),clips:l.map(({id:e,mediaId:t,kind:n,track:i,start:r,in:a,duration:o,name:s,props:l,keyframes:c,transitionIn:d,transitionOut:u,linkedId:p,linkGroup:m})=>{const f={id:e,mediaId:t,kind:n,track:i,start:r,in:a,duration:o,name:s,props:l,keyframes:c,transitionIn:d,transitionOut:u};return m&&(f.linkGroup=m),p&&(f.linkedId=p),f}),markers:(c||[]).map(({t:e,label:t})=>t?{t:e,label:t}:{t:e}),inPoint:d??null,outPoint:u??null,disabledTracks:normalizeDisabledTracks(p)}}function listenSSE(){runtime.serverEvents?.close();const e=new EventSource(projectApi("/api/events"));runtime.serverEvents=e,e.onmessage=e=>{"projects"===e.data?(loadProjectCatalog().catch(()=>{}),syncFromServer()):syncFromServer()},e.onerror=()=>{runtime.connecting||syncFromServer()}}async function syncFromServer(e){if(runtime.connecting)return;if(state.gesture||state.exporting||$("coverDialog").open)return void(runtime.pendingSync=!0);runtime.pendingSync=!1,"project"!==state.binTab&&fetchLibrary(state.binTab).then(renderLibrary),loadLibraryFonts();const t=runtime.projectReadVersion=(runtime.projectReadVersion||0)+1;try{const n=await fetchProjectDocument();if(t!==runtime.projectReadVersion)return;if($("coverDialog").open)return void(runtime.pendingSync=!0);if(!e&&!runtime.projectLoadError&&(n.revision||0)===(project.revision||0))return;runtime.saveTimer&&(clearTimeout(runtime.saveTimer),runtime.saveTimer=null,toast(uiText("Project was updated externally — your last change may need redoing."))),applyProject(n),clearProjectLoadError(),await probeMissingMeta()}catch(e){t===runtime.projectReadVersion&&showProjectLoadError(e)}}async function loadMediaMetadata(e){const t=e.src,n=mediaSourceVersions.get(t);try{if("svg"===e.kind)return void await loadSvgMedia({...e,src:mediaLoadSrc(e)});if("image"===e.kind){const t=await loadImage(mediaLoadSrc(e));return runtime.mediaAux.set(e.id,{...runtime.mediaAux.get(e.id)||{},img:t}),e.width=t.naturalWidth,void(e.height=t.naturalHeight)}Object.assign(e,await probeAV(mediaLoadSrc(e),e.kind))}catch(e){throw mediaSourceVersions.get(t)===n&&setMediaFailure(t,"unreadable"),e}}const mediaFailures=new Map,mediaSourceVersions=new Map;function mediaLoadSrc(e){if(!e?.src)return"";const t=mediaSourceVersions.get(e.src);if(!t)return e.src;const n=new URL(e.src,location.href);return n.origin===location.origin&&/^https?:$/.test(n.protocol)?(n.searchParams.set("_fcRetry",t),n.href):e.src}let mediaCheckPromise=null;function mediaFailure(e){return e?.src?mediaFailures.get(e.src):"missing"}function clipMediaFailure(e){return["video","audio","image","svg"].includes(e.kind)?mediaFailure(getMedia(e.mediaId)):null}function mediaFailureLabel(e){return uiText("missing"===e?"Media missing":"Media unreadable")}function setMediaFailure(e,t){e&&project.media.some(t=>t.src===e)&&("unreadable"===t&&"missing"===mediaFailures.get(e)||mediaFailures.get(e)!==t&&(t?mediaFailures.set(e,t):mediaFailures.delete(e),t&&(clipThumbQueue=clipThumbQueue.filter(t=>t.src!==e)),renderBin(),state.dirtyTimeline=!0))}function resetMediaSource(e){mediaSourceVersions.set(e,String(Date.now()));for(const t of project.media.filter(t=>t.src===e)){runtime.mediaAux.delete(t.id),runtime.audioBufs.delete(t.id),runtime.wavePeaks.delete(t.id);for(const e of project.clips.filter(e=>e.mediaId===t.id))releaseClipEl(e.id)}for(const[t,n]of clipThumbCache)n.src===e&&clipThumbCache.delete(t);clipThumbQueue=clipThumbQueue.filter(t=>t.src!==e),clipThumbIndex=null,resetPreviewWait()}async function retryMediaSource(e){const t=e.src;resetMediaSource(t),mediaFailures.delete(t);for(const e of project.media.filter(e=>e.src===t))try{await loadMediaMetadata({...e})}catch{setMediaFailure(t,"unreadable")}renderBin(),state.dirtyTimeline=!0}async function checkMediaAvailability(e=!1){if(mediaCheckPromise||state.rendering||state.exporting&&!backgroundExportActive)return mediaCheckPromise;mediaCheckPromise=(async()=>{const t=[...new Map(project.media.filter(e=>e.src).map(e=>[e.src,e])).values()];await Promise.all(Array.from({length:Math.min(4,t.length)},async()=>{for(;t.length;){const n=t.shift(),i=n.src;try{const t=new URL(i,location.href);if(t.origin!==location.origin||!/^https?:$/.test(t.protocol)){e&&mediaFailures.has(i)&&await retryMediaSource(n);continue}const r=await fetch(t,{method:"HEAD",cache:"no-store",signal:AbortSignal.timeout(5e3)});if(!project.media.includes(n)||n.src!==i)continue;404===r.status||410===r.status?setMediaFailure(i,"missing"):r.ok||405===r.status||501===r.status?(r.ok||405===r.status||501===r.status)&&("missing"===mediaFailures.get(i)||e&&mediaFailures.has(i))&&await retryMediaSource(n):setMediaFailure(i,"unreadable")}catch{project.media.includes(n)&&n.src===i&&setMediaFailure(i,"unreadable")}}}))})();try{await mediaCheckPromise}finally{mediaCheckPromise=null}}function updateMissingMediaNotice(){const e=$("missingMediaNotice"),t=previewFrameTime(runtime.previewWait?.t??state.time),n=project.clips.filter(e=>isTrackEnabled(e.track)&&activeAt(e,t)&&clipMediaFailure(e));if(e.hidden=!n.length||state.rendering||state.exporting&&!backgroundExportActive,e.hidden)return;const i=n.some(e=>"missing"===clipMediaFailure(e)),r=mediaFailureLabel(i?"missing":"unreadable"),a=[...new Set(n.map(e=>getMedia(e.mediaId)?.name||e.name||e.mediaId||e.id))].join("、"),o=JSON.stringify([r,a]);e.dataset.key!==o&&(e.dataset.key=o,$("missingMediaTitle").textContent=r,$("missingMediaNames").textContent=a,$("missingMediaNames").title=a,$("missingMediaHelp").textContent=uiText(i?"The source file was deleted or moved. Restore it to its original location, then retry.":"The source file could not be loaded. Check the file or connection, then retry."))}async function probeMissingMeta(){checkMediaAvailability();let e=!1;for(const t of project.media)if("svg"!==t.kind){if("image"!==t.kind&&(null==t.duration||isNaN(t.duration)))try{await loadMediaMetadata(t),e=!0}catch{}if("image"===t.kind&&!runtime.mediaAux.get(t.id)?.img)try{await loadMediaMetadata(t),e=!0}catch{}"video"!==t.kind||runtime.mediaAux.get(t.id)?.thumb||grabThumb(t).catch(()=>{}),ensureWave(t)}else if(!runtime.mediaAux.get(t.id)?.svgText)try{await loadMediaMetadata(t),e=!0}catch{}e&&(renderBin(),scheduleSave()),state.dirtyTimeline=!0}function probeAV(e,t){return new Promise((n,i)=>{const r=document.createElement("audio"===t?"audio":"video"),a=(e,t)=>{clearTimeout(o),r.onloadedmetadata=r.onerror=null,r.removeAttribute("src"),r.load(),e?i(e):n(t)},o=setTimeout(()=>a(new Error("media metadata timed out")),1e4);r.onloadedmetadata=()=>a(null,{duration:r.duration,width:r.videoWidth||void 0,height:r.videoHeight||void 0}),r.onerror=()=>a(new Error(r.error?.message||"media load failed")),r.preload="metadata",r.src=e})}function loadImage(e){return new Promise((t,n)=>{const i=new Image,r=e=>{clearTimeout(a),i.onload=i.onerror=null,e?n(e):t(i)},a=setTimeout(()=>r(new Error("image load timed out")),1e4);i.onload=()=>r(),i.onerror=()=>r(new Error("image load failed")),i.src=e})}async function grabThumb(e){const t=e.src,n=mediaSourceVersions.get(t),i=document.createElement("video");i.muted=!0,i.preload="auto",i.src=mediaLoadSrc(e);try{await new Promise((e,t)=>{i.onloadeddata=e,i.onerror=t}),i.currentTime=Math.min(.5,(i.duration||1)/2),await new Promise(e=>{i.onseeked=e,setTimeout(e,1500)}),runtime.mediaAux.set(e.id,{...runtime.mediaAux.get(e.id)||{},thumb:videoThumb(i)}),renderBin(),state.dirtyTimeline=!0}catch(e){throw i.error&&mediaSourceVersions.get(t)===n&&setMediaFailure(t,"unreadable"),e}finally{i.pause(),i.removeAttribute("src"),i.load()}}function videoThumb(e){const t=document.createElement("canvas"),n=Math.min(160/e.videoWidth,160/e.videoHeight,1);return t.width=Math.max(1,Math.round(e.videoWidth*n)),t.height=Math.max(1,Math.round(e.videoHeight*n)),t.getContext("2d").drawImage(e,0,0,t.width,t.height),t.toDataURL("image/jpeg",.6)}$("btnRetryMedia").addEventListener("click",async()=>{const e=$("btnRetryMedia");e.disabled=!0;try{await mediaCheckPromise,await checkMediaAvailability(!0)}finally{e.disabled=!1}}),window.addEventListener("focus",()=>{checkMediaAvailability()}),setInterval(()=>{previewUnavailable()||checkMediaAvailability()},1e4);const clipThumbCache=new Map;let clipThumbIndex=null,clipThumbQueue=[],clipThumbActive=0,visibleThumbKeys=new Set,neededThumbKeys=new Set,clipThumbsChanged=!1,clipThumbProgressDirty=!0;const clipThumbVideos=[],clipThumbDecodes=new Set;let clipThumbDecodeWaiters=0;function releaseClipThumbVideo(e){e.video.pause(),e.video.removeAttribute("src"),e.video.load()}function acquireClipThumbVideo(e){const t=clipThumbVideos.findIndex(t=>t.src===e),n=clipThumbVideos.splice(t<0?0:t,1)[0]||{video:document.createElement("video"),src:null};return n.src!==e&&(n.video.muted=!0,n.video.preload="auto",n.video.src=e,n.video.load(),n.src=e),n}const clipThumbDiskSources=new Map;let clipThumbDiskCache,clipThumbDiskDisabled=!1,clipThumbDiskWrites=0,clipThumbDiskWrite=Promise.resolve();const clipThumbDiskQueue=new Map;let clipThumbDiskWriting=!1;async function withClipThumbStorage(e){if(clipThumbDiskDisabled||"undefined"==typeof caches)return null;let t;try{return await Promise.race([e(),new Promise(e=>{t=setTimeout(()=>{clipThumbDiskDisabled=!0,e(null)},2e3)})])}catch{return clipThumbDiskDisabled=!0,null}finally{clearTimeout(t)}}function validStoredClipThumb(e){return"string"==typeof e&&e.length<=65536&&/^data:image\/jpeg;base64,[A-Za-z0-9+/]+=*$/.test(e)}async function clipThumbSourceVersion(e){const t=new URL(e,location.href);if(t.origin!==location.origin||!/^https?:$/.test(t.protocol))return null;const n=mediaLoadSrc({src:e});return clipThumbDiskSources.has(n)||clipThumbDiskSources.set(n,(async()=>{try{const e=await fetch(n,{method:"HEAD",cache:"no-store",signal:AbortSignal.timeout(1500)}),t=e.headers.get("ETag");return e.ok&&t?t:null}catch{return null}})()),clipThumbDiskSources.get(n)}async function readStoredClipThumb(e){return withClipThumbStorage(async()=>{const t=new URL(e.src,location.href),n=await clipThumbSourceVersion(e.src);if(!n||clipThumbDiskDisabled)return null;const i=await(clipThumbDiskCache||=caches.open("fablecut-timeline-thumbs-v1")),r=location.origin+"/__fablecut_thumbnails__/"+encodeURIComponent(JSON.stringify([t.href,n,e.time]));e.disk={cache:i,key:r};const a=await i.match(r),o=a?await a.text():null;if(validStoredClipThumb(o)){const e=new Image;e.src=o;try{if(await e.decode(),e.naturalWidth&&e.naturalHeight)return o}catch{}}return a&&await i.delete(r),null})}function storeClipThumb(e){if(clipThumbDiskDisabled||!e.disk||!validStoredClipThumb(e.thumb))return;const{cache:t,key:n}=e.disk;return clipThumbDiskQueue.delete(n),clipThumbDiskQueue.set(n,{cache:t,key:n,thumb:e.thumb}),clipThumbDiskQueue.size>1024&&clipThumbDiskQueue.delete(clipThumbDiskQueue.keys().next().value),clipThumbDiskWriting||(clipThumbDiskWriting=!0,clipThumbDiskWrite=(async()=>{try{for(;clipThumbDiskQueue.size&&!clipThumbDiskDisabled;){const e=clipThumbDiskQueue.values().next().value;clipThumbDiskQueue.delete(e.key),await withClipThumbStorage(async()=>{if(await e.cache.put(e.key,new Response(e.thumb)),clipThumbDiskWrites++%32==0){const t=await e.cache.keys();for(const n of t.slice(0,Math.max(0,t.length-992)))await e.cache.delete(n)}})}}finally{clipThumbDiskQueue.clear(),clipThumbDiskWriting=!1}})()),clipThumbDiskWrite}function clipThumbKey(e,t=0){const n=getMedia(e.mediaId);if("video"!==e.kind||!n?.src||mediaFailures.has(n.src))return null;const i=mediaTimeAt(e,e.start+clamp(t,0,Math.max(0,e.duration-.001)));return JSON.stringify([n.src,+Math.max(0,i||0).toFixed(3)])}function nearbyClipThumb(e,t){const n=clipThumbKey(e,t);if(!n)return null;const[i,r]=JSON.parse(n);if(!clipThumbIndex){clipThumbIndex=new Map;for(const e of clipThumbCache.values())e.thumb&&(clipThumbIndex.has(e.src)||clipThumbIndex.set(e.src,[]),clipThumbIndex.get(e.src).push(e));for(const e of clipThumbIndex.values())e.sort((e,t)=>e.time-t.time)}const a=clipThumbIndex.get(i);if(!a?.length)return null;const o=JSON.parse(clipThumbKey(e,0))[1],s=JSON.parse(clipThumbKey(e,e.duration))[1];let l=0,c=a.length;for(;l<c;){const e=l+c>>>1;a[e].time<r?l=e+1:c=e}let d=null;for(const e of[a[l-1],a[l]])!e||e.time<o||e.time>s||clipThumbCache.get(e.key)!==e||(!d||Math.abs(e.time-r)<Math.abs(d.time-r))&&(d=e);return d?(neededThumbKeys.add(d.key),d.thumb):null}function pruneClipThumbs(){clipThumbProgressDirty=!0;const e=new Set(project.media.map(e=>e.src));for(const[t,n]of clipThumbCache)(!e.has(n.src)||clipThumbCache.size>512&&!neededThumbKeys.has(t))&&clipThumbCache.delete(t);for(const e of clipThumbQueue)neededThumbKeys.has(e.key)||clipThumbCache.delete(e.key);clipThumbQueue=clipThumbQueue.filter(e=>clipThumbCache.get(e.key)===e),clipThumbQueue.sort((e,t)=>Number(visibleThumbKeys.has(t.key))-Number(visibleThumbKeys.has(e.key))),clipThumbIndex=null}function clipThumb(e,t=0,n=!0){const i=clipThumbKey(e,t);if(!i)return null;neededThumbKeys.add(i),n&&visibleThumbKeys.add(i);let r=clipThumbCache.get(i);if(r)clipThumbCache.delete(i),clipThumbCache.set(i,r);else{const[t,a]=JSON.parse(i);r={key:i,src:t,time:a,clip:{id:e.id,mediaId:e.mediaId},thumb:null},clipThumbCache.set(i,r),clipThumbQueue.push(r),clipThumbProgressDirty=!0,n&&loadClipThumbs()}return r.thumb}function clipThumbWidth(e,t){const n=getMedia(e.mediaId),i=Math.max(8,t-8);return Math.max(16,i*((n?.width||16)/(n?.height||9)))}function preloadClipThumbs(){if(trackSizeShowsThumbs())for(const e of project.clips){const t=trackOf(e);if("video"!==e.kind||!t)continue;const n=clipThumbWidth(e,t.h),i=Math.ceil(e.duration*state.pps/n);for(let t=0;t<i;t++){const i=t*n/state.pps;clipThumb(e,i,!1)||nearbyClipThumb(e,i)}}}function clipThumbStrip(e,t,n=state.pps,i=els.timelineScroll){const r=clipThumbWidth(e,t),a=e.duration*n,o=e.start*n,s=Math.max(0,Math.floor((i.scrollLeft-o)/r)),l=Math.min(Math.ceil(a/r),Math.ceil((i.scrollLeft+i.clientWidth-o)/r));let c="";for(let t=s;t<l;t++){const i=t*r/n,a=clipThumb(e,i)||nearbyClipThumb(e,i);c+=`<div class="thumbs" data-thumb-key="${encodeURIComponent(clipThumbKey(e,i))}" style="left:${t*r}px;width:${r+1}px;${a?`background-image:url('${a}')`:""}"></div>`}return c?`<div class="clip-thumb-strip">${c}</div>`:""}function loadClipThumbs(){if(state.exporting||state.rendering||previewUnavailable()||!clipThumbQueue.length)clipThumbVideos.splice(0).forEach(releaseClipThumbVideo);else for(;clipThumbActive<2&&clipThumbQueue.length&&!clipThumbDecodeWaiters;){const e=clipThumbQueue.shift();clipThumbActive++,(async()=>{let t,n,i,r=!1;try{const a="undefined"!=typeof caches?await readStoredClipThumb(e):null;if(clipThumbCache.get(e.key)!==e)return;if(!neededThumbKeys.has(e.key))return void clipThumbCache.delete(e.key);if(a)return e.thumb=a,void(clipThumbIndex=null);if(state.playing&&clipThumbDecodes.size){clipThumbDecodeWaiters++;try{for(;state.playing&&clipThumbDecodes.size;)await Promise.race(clipThumbDecodes)}finally{clipThumbDecodeWaiters--}}if(clipThumbCache.get(e.key)!==e)return;if(!neededThumbKeys.has(e.key))return void clipThumbCache.delete(e.key);if(state.exporting||state.rendering||previewUnavailable())return void clipThumbQueue.unshift(e);n=new Promise(e=>{i=e}),clipThumbDecodes.add(n),t=acquireClipThumbVideo(mediaLoadSrc({src:e.src}));const o=t.video;await prepareVideoFrame(e.clip,o,e.time),r=!0,clipThumbCache.get(e.key)===e&&(e.thumb=videoThumb(o),clipThumbIndex=null,storeClipThumb(e))}catch{e.failed=!0}finally{n&&(clipThumbDecodes.delete(n),i()),t&&(r&&clipThumbQueue.length?clipThumbVideos.push(t):releaseClipThumbVideo(t)),clipThumbActive--,clipThumbProgressDirty=!0,clipThumbCache.get(e.key)===e&&e.thumb&&(clipThumbsChanged=!0),loadClipThumbs()}})()}}function clipThumbProgress(){let e=0,t=0,n=0;for(const i of neededThumbKeys){const r=clipThumbCache.get(i);r&&!mediaFailures.has(r.src)&&(e++,r.thumb?t++:r.failed&&n++)}return{total:e,loaded:t,failed:n,pending:e-t-n}}function renderClipThumbProgress(){if(!clipThumbProgressDirty||!els.thumbProgress)return;clipThumbProgressDirty=!1;const e=els.timelineScroll.clientHeight,{total:t,loaded:n,failed:i,pending:r}=clipThumbProgress();els.thumbProgress.hidden=!r&&!i,els.thumbProgressBar.hidden=!r,els.thumbProgressBar.max=Math.max(1,t),els.thumbProgressBar.value=n,els.thumbProgressLabel.textContent=uiText(r?"Thumbnails {loaded}/{total}":"{failed} thumbnails unavailable",{loaded:n,total:t,failed:i}),els.timelineScroll.clientHeight!==e&&(state.dirtyTimeline=!0)}function updateClipThumbs(){if(clipThumbsChanged){clipThumbsChanged=!1;for(const e of[...els.tracksContent.querySelectorAll(".thumbs[data-thumb-key]"),...els.coverFrames?.querySelectorAll(".thumbs[data-thumb-key]")||[]]){const t=clipThumbCache.get(decodeURIComponent(e.dataset.thumbKey))?.thumb;if(t){const n=`url(${JSON.stringify(t)})`;e.style.backgroundImage!==n&&(e.style.backgroundImage=n)}}}renderClipThumbProgress(),loadClipThumbs()}let decodeCtx=null;function getDecodeCtx(){return decodeCtx||(decodeCtx=new(window.AudioContext||window.webkitAudioContext))}function getAudioBuffer(e){let t=runtime.audioBufs.get(e.id);return t||(t=fetch(mediaLoadSrc(e)).then(e=>e.arrayBuffer()).then(e=>getDecodeCtx().decodeAudioData(e)),runtime.audioBufs.set(e.id,t),t.catch(()=>runtime.audioBufs.delete(e.id))),t}const BROWSER_DECODE_MAX_SECONDS=600;function browserDecodable(e){return Number.isFinite(e.duration)&&e.duration<=600}async function serverWave(e){const t=await fetch(projectApi("/api/waveform?src="+encodeURIComponent(e.src)),{cache:"no-store"}),n=await t.json();if(!t.ok||50!==n.peaksPerSecond)throw new Error(n.error||"waveform unavailable");return{channels:n.peaks.map(e=>{const t=Uint8Array.from(atob(e),e=>e.charCodeAt(0));return Float32Array.from(t,e=>e/255)}),numberOfChannels:n.channels}}async function browserWave(e){const t=await getAudioBuffer(e),n=Math.max(1,Math.ceil(50*t.duration)),i=t.length/n,r=[];for(let e=0;e<t.numberOfChannels;e++){const a=t.getChannelData(e),o=new Float32Array(n);for(let e=0;e<n;e++){let t=0;const n=Math.floor(e*i),r=Math.min(a.length,Math.floor((e+1)*i));for(let e=n;e<r;e+=8){const n=Math.abs(a[e]);n>t&&(t=n)}o[e]=t}r.push(o)}return{channels:r,numberOfChannels:t.numberOfChannels}}function ensureWave(e){"audio"!==e.kind&&"video"!==e.kind||runtime.wavePeaks.has(e.id)||project.clips.some(t=>"audio"===t.kind&&t.mediaId===e.id)&&(runtime.wavePeaks.set(e.id,null),serverWave(e).catch(t=>{if(!browserDecodable(e))throw t;return browserWave(e)}).then(({channels:t,numberOfChannels:n})=>{const i=Math.max(1,...t.map(e=>e.length)),r=new Float32Array(i);for(let e=0;e<i;e++){let n=0;for(const i of t)i[e]>n&&(n=i[e]);r[e]=n}runtime.wavePeaks.set(e.id,{channels:t,max:r}),null==e.channels&&(e.channels=n),state.dirtyTimeline=!0}).catch(()=>runtime.wavePeaks.delete(e.id)))}function wavePeaksFor(e){const t=runtime.wavePeaks.get(e.mediaId);if(!t)return null;if(t instanceof Float32Array)return t;if(!t.max)return null;const n=e.props?.audioChannel;return Number.isInteger(n)&&t.channels?.[n]?t.channels[n]:t.max}function mediaKindFromFile(e){const t=e.type||"";if("image/svg+xml"===t||"image/svg"===t)return"svg";if(t.startsWith("video/"))return"video";if(t.startsWith("audio/"))return"audio";if(t.startsWith("image/"))return"image";const n=(e.name.split(".").pop()||"").toLowerCase();return"svg"===n?"svg":["mp4","mov","webm","mkv","m4v","avi","mpg","mpeg"].includes(n)?"video":["mp3","wav","m4a","aac","ogg","flac","wma"].includes(n)?"audio":["png","jpg","jpeg","gif","webp","bmp","avif"].includes(n)?"image":null}async function importFiles(e){if(runtime.projectLoadError)return[];const t=runtime.projectEpoch,n=[...e];if(!n.length)return[];const i=runtime.importFolderId&&getFolder(runtime.importFolderId)?runtime.importFolderId:null;runtime.importFolderId=null;let r=0,a=0;const o=[];for(const e of n){if(runtime.projectLoadError||runtime.projectEpoch!==t)break;const n=mediaKindFromFile(e);if(!n){a++;continue}let s,l=!1;if(state.connected)try{const t=await fetch(projectApi("/api/upload?name="+encodeURIComponent(e.name)),{method:"POST",body:e});if(!t.ok)throw new Error("upload "+t.status);s=(await t.json()).src}catch{s=URL.createObjectURL(e),l=!0}else s=URL.createObjectURL(e),l=!0;const c={id:"m_"+uid(),name:e.name,kind:n,src:s,transient:l,folderId:i};try{await loadMediaMetadata(c),"video"===n&&grabThumb(c).catch(()=>{}),ensureWave(c)}catch{a++;continue}if(runtime.projectLoadError||runtime.projectEpoch!==t){l&&URL.revokeObjectURL(s);break}project.media.push(c),o.push(c),r++}return renderBin(),scheduleSave(),!r&&a?toast(uiText("Couldn't import — unsupported or unreadable file type")):a&&toast(uiText("Imported {added}, skipped {skipped}",{added:r,skipped:a})),o}function renderBin(){els.binList.querySelectorAll(".bin-item, .bin-folder, .bin-drop-root").forEach(e=>e.remove());const e=!project.media.length&&!project.folders.length;if(els.binEmpty.style.display=e?"":"none",e)return;const t=()=>{els.binList.querySelectorAll(".bin-drop-over").forEach(e=>e.classList.remove("bin-drop-over"))},n=(e,n)=>{e.addEventListener("dragover",i=>{const r=[...i.dataTransfer?.types||[]],a=r.includes("text/fablecut-media"),o=r.includes("text/fablecut-folder"),s=r.includes("Files");if(a||o||s){if(o){const e=runtime.binDragFolderId;if(e&&n&&isSelfOrFolderAncestor(e,n))return}i.preventDefault(),i.stopPropagation(),i.dataTransfer.dropEffect=s?"copy":"move",t(),e.classList.add("bin-drop-over"),s&&(runtime.importFolderId=n)}}),e.addEventListener("dragleave",t=>{e.contains(t.relatedTarget)||(e.classList.remove("bin-drop-over"),runtime.importFolderId===n&&(runtime.importFolderId=null))}),e.addEventListener("drop",e=>{const i=e.dataTransfer.getData("text/fablecut-media"),r=e.dataTransfer.getData("text/fablecut-folder"),a=e.dataTransfer.files;return t(),a?.length?(e.preventDefault(),e.stopPropagation(),runtime.importFolderId=n,void importFiles(a)):i?(e.preventDefault(),e.stopPropagation(),void moveMediaToFolder(i,n)):void(r&&(e.preventDefault(),e.stopPropagation(),moveFolderToParent(r,n)))})},i=(e,n)=>{const i=document.createElement("div"),r=mediaFailure(e);i.className="bin-item"+(r?" media-missing":""),i.draggable=!0,i.dataset.mediaId=e.id,i.style.setProperty("--bin-depth",n);const a=runtime.mediaAux.get(e.id)||{},o=mediaKindIcon(e.kind,"🎞"),s=a.thumb||("image"===e.kind||"svg"===e.kind?e.src:null);i.innerHTML=`\n <div class="bin-thumb${"video"===e.kind?" bin-thumb-video":""}"></div>\n <div class="bin-meta">\n <div class="bin-name"></div>\n <div class="bin-sub">${uiText(e.kind)}${e.duration?" · "+fmt(e.duration):""}</div>\n </div>\n <span class="bin-del" title="${uiAttr("Remove (and its clips)")}">✕</span>`;const l=i.querySelector(".bin-thumb");r?(l.textContent="⚠",i.querySelector(".bin-sub").textContent=mediaFailureLabel(r)):s?l.style.backgroundImage=`url(${JSON.stringify(s)})`:l.textContent=o;const c=i.querySelector(".bin-name");return c.textContent=e.name,c.title=e.name,i.addEventListener("dragstart",t=>{runtime.binDragFolderId=null,runtime.dragMediaId=e.id,t.dataTransfer.setData("text/fablecut-media",e.id),t.dataTransfer.effectAllowed="copyMove",i.classList.add("bin-dragging")}),i.addEventListener("dragend",()=>{i.classList.remove("bin-dragging"),t(),runtime.importFolderId=null}),i.addEventListener("dragover",e=>e.stopPropagation()),i.addEventListener("click",t=>{t.target.closest(".bin-del")||t.ctrlKey||t.metaKey||getSetting("linkSelect")&&(t.preventDefault(),selectClipsByMediaId(e.id))}),i.addEventListener("dblclick",()=>addClipFromMedia(e,null,state.time)),i.querySelector(".bin-del").addEventListener("click",t=>{t.stopPropagation(),pushUndo(),project.media=project.media.filter(t=>t.id!==e.id),project.clips=project.clips.filter(t=>t.mediaId!==e.id),renderBin(),scheduleSave(),renderInspector()}),i},r=(e,a)=>{for(const i of folderChildren(e)){const e=document.createElement("div");e.className="bin-folder"+(i.open?" open":""),e.dataset.folderId=i.id,e.draggable=!0,e.style.setProperty("--bin-depth",a);const o=mediaInFolder(i.id).length+folderChildren(i.id).length;e.innerHTML=`\n <button type="button" class="bin-folder-twist" title="${i.open?uiText("Collapse"):uiText("Expand")}" aria-expanded="${i.open}">${i.open?"▼":"▶"}</button>\n <span class="bin-folder-icon">📁</span>\n <span class="bin-folder-name"></span>\n <span class="bin-folder-count">${o}</span>\n <button type="button" class="bin-folder-add" title="${uiAttr("New subfolder")}">+</button>\n <button type="button" class="bin-del bin-folder-del" title="${uiAttr("Delete folder (keeps media)")}">✕</button>`;const s=e.querySelector(".bin-folder-name");s.textContent=i.name,s.title=i.name,e.querySelector(".bin-folder-twist").addEventListener("click",e=>{e.stopPropagation(),i.open=!i.open,scheduleSave(),renderBin()}),e.querySelector(".bin-folder-name").addEventListener("dblclick",e=>{e.stopPropagation(),startFolderRename(i.id)}),e.querySelector(".bin-folder-add").addEventListener("click",e=>{e.stopPropagation(),addFolder(i.id)}),e.querySelector(".bin-folder-del").addEventListener("click",e=>{e.stopPropagation(),deleteFolder(i.id)}),e.addEventListener("dragstart",t=>{t.target.closest("button")?t.preventDefault():(runtime.binDragFolderId=i.id,t.dataTransfer.setData("text/fablecut-folder",i.id),t.dataTransfer.effectAllowed="move",e.classList.add("bin-dragging"))}),e.addEventListener("dragend",()=>{runtime.binDragFolderId=null,e.classList.remove("bin-dragging"),t(),runtime.importFolderId=null}),n(e,i.id),els.binList.appendChild(e),i.open&&r(i.id,a+1)}for(const t of mediaInFolder(e))els.binList.appendChild(i(t,a))};r(null,0);const a=document.createElement("div");a.className="bin-drop-root",a.textContent=uiText("Drop here to move to root"),n(a,null),els.binList.appendChild(a),syncBinSelectionFromTimeline()}function openFileImport(e,t){const n=els.fileInput;null!=e&&null!=t&&(n.style.cssText=`position:fixed;left:${e}px;top:${t}px;width:1px;height:1px;opacity:0;margin:0;padding:0;border:0;overflow:hidden;z-index:-1;`),setTimeout(()=>n.click(),0)}async function fetchLibrary(e){try{runtime.library[e]=await(await fetch("/api/library?dir="+e)).json()}catch{runtime.library[e]=[]}return runtime.library[e]}function libKind(e){return/\.svg$/i.test(e)?"svg":AUDIO_EXT.test(e)?"audio":VIDEO_EXT.test(e)?"video":IMAGE_EXT.test(e)?"image":null}function mediaKindIcon(e,t){return"audio"===e?"🎵":"svg"===e?"✨":"image"===e?"🖼":"video"===e?"🎞":t}function mediaForLibraryItem(e){let t=project.media.find(t=>t.src===e.src);if(t)return t;const n=libKind(e.name);return n?(t={id:"m_"+uid(),name:e.name,kind:n,src:e.src,folderId:null},project.media.push(t),renderBin(),scheduleSave(),t):null}async function addLibraryItem(e,t,n,i){const r=mediaForLibraryItem(e);if(r){if(("audio"===r.kind||"video"===r.kind)&&(null==r.duration||isNaN(r.duration))){try{await loadMediaMetadata(r)}catch{}ensureWave(r),"video"===r.kind&&grabThumb(r).catch(()=>{})}if("svg"===r.kind&&!runtime.mediaAux.get(r.id)?.svgText)try{await loadMediaMetadata(r)}catch{}if("image"===r.kind&&!runtime.mediaAux.get(r.id)?.img)try{await loadMediaMetadata(r)}catch{}addClipFromMedia(r,t,n,i)}else toast(uiText("Unsupported file type: ")+e.name)}function toggleSfxPreview(e,t){if(previewUnavailable())return;const n=runtime.sfxPreview;if(n&&n.dataset.src===e.src&&!n.paused)return n.pause(),void(t.textContent="▶");n&&n.pause(),els.libList.querySelectorAll(".lib-play").forEach(e=>e.textContent="▶");const i=new Audio(e.src);i.dataset.src=e.src,i.onended=()=>{t.textContent="▶"},i.play().catch(()=>toast(uiText("Couldn't play ")+e.name)),t.textContent="⏸",runtime.sfxPreview=i}function renderLibrary(){const e=state.binTab;if("project"===e)return;const t=runtime.library[e]||[];if(els.libList.innerHTML="",t.length)for(const n of t){const t=libKind(n.name),i=document.createElement("div");i.className="bin-item lib-item",i.draggable=!0;const r="image"===t||"svg"===t,a=mediaKindIcon(t,"🧩");i.innerHTML=`\n <div class="bin-thumb${"svg"===t?" svg":""}"></div>\n <div class="bin-meta">\n <div class="bin-name"></div>\n <div class="bin-sub">${(n.size/1024).toFixed(0)} KB</div>\n </div>\n ${"sfx"===e?`<button class="btn tiny lib-play" title="${uiAttr("Preview")}">▶</button>`:""}\n <button class="btn tiny accent lib-add" title="${uiAttr("Add at playhead")}">+</button>`;const o=i.querySelector(".bin-thumb");r?o.style.backgroundImage=`url(${JSON.stringify(n.src)})`:o.textContent=a;const s=i.querySelector(".bin-name");s.textContent=n.name,s.title=n.rel||n.name,i.addEventListener("dragstart",e=>{const t=mediaForLibraryItem(n);t?(runtime.dragMediaId=t.id,e.dataTransfer.setData("text/fablecut-media",t.id),e.dataTransfer.effectAllowed="copy"):e.preventDefault()}),i.addEventListener("dblclick",()=>addLibraryItem(n,null,state.time)),i.querySelector(".lib-add").addEventListener("click",()=>addLibraryItem(n,null,state.time));const l=i.querySelector(".lib-play");l&&l.addEventListener("click",()=>toggleSfxPreview(n,l)),els.libList.appendChild(i)}else els.libList.innerHTML=`<div class="bin-empty">\n <div class="bin-empty-icon">${"sfx"===e?"🔊":"svg"===e?"✨":"🧩"}</div>\n <p>${uiText("No assets yet.")}</p>\n <p class="hint">${uiText("Drop files into")}<br><b>library/${e}/</b><br>${uiText("— this list live-updates.")}</p></div>`}function setBinTab(e){state.binTab=e;for(const t of els.binTabs.querySelectorAll("[data-tab]"))t.classList.toggle("on",t.dataset.tab===e);const t="project"===e;els.binList.classList.toggle("hidden",!t),els.libList.classList.toggle("hidden",t),t||fetchLibrary(e).then(renderLibrary)}function syncHistoryButtons(){$("btnUndo").disabled=0===runtime.undo.length,$("btnRedo").disabled=0===runtime.redo.length}function pushUndo(){runtime.undo.push(timelineSnapshot()),runtime.undo.length>100&&runtime.undo.shift(),runtime.redo.length=0,syncHistoryButtons()}function undo(){runtime.undo.length&&(runtime.redo.push(timelineSnapshot()),restoreTimeline(runtime.undo.pop()),pruneSelection(),scheduleSave(),renderInspector())}function redo(){runtime.redo.length&&(runtime.undo.push(timelineSnapshot()),restoreTimeline(runtime.redo.pop()),pruneSelection(),scheduleSave(),renderInspector())}function defaultTrackFor(e){return"audio"===e?"A1":"svg"===e?"V3":"V1"}function linkedClip(e){return e?.linkedId?getClip(e.linkedId):null}function withLinked(e){const t=new Map,n=new Set;for(const i of e)if(t.set(i.id,i),i.linkGroup)n.add(i.linkGroup);else{const e=linkedClip(i);e&&t.set(e.id,e)}if(n.size)for(const e of project.clips)e.linkGroup&&n.has(e.linkGroup)&&t.set(e.id,e);return[...t.values()]}function syncLinkedTiming(e){for(const t of withLinked([e]))t.id!==e.id&&(t.start=e.start,t.in=e.in,t.duration=e.duration,null!=e.props?.speed&&(t.props=t.props||{},t.props.speed=e.props.speed))}function syncLinkedSpeed(e){for(const t of withLinked([e]))t.id!==e.id&&(t.props=t.props||{},t.props.speed=clipSpeed(e),e.keyframes?.speed?.length?(t.keyframes=t.keyframes||{},t.keyframes.speed=e.keyframes.speed.map(e=>({...e}))):t.keyframes?.speed&&(delete t.keyframes.speed,Object.keys(t.keyframes).length||(t.keyframes=void 0)));state.dirtyTimeline=!0}function relinkClips(){const e=(e,t)=>Math.abs((+e||0)-(+t||0))<.001;for(const e of project.clips){if(e.linkedId&&!e.linkGroup){const t=getClip(e.linkedId);t&&(e.linkGroup=t.linkGroup=t.linkGroup||"lg_"+uid())}delete e.linkedId}const t=project.clips.filter(e=>"audio"===e.kind&&e.mediaId),n=new Set;for(const i of project.clips){if("video"!==i.kind||!i.mediaId||i.linkGroup)continue;const r=t.filter(t=>!n.has(t.id)&&!t.linkGroup&&t.mediaId===i.mediaId&&e(t.start,i.start)&&e(t.in,i.in)&&e(t.duration,i.duration));if(!r.length)continue;const a="lg_"+uid();i.linkGroup=a;for(const e of r)e.linkGroup=a,n.add(e.id)}}function addClipFromMedia(e,t,n,i){pushUndo();const r=e.kind;i?.newTrack&&(t=Timeline.insertTrack(project,Timeline.kindOf(e),i.beforeId)),ensureTrack(t=t||defaultTrackFor(r));const a=TRACKS.find(e=>e.id===t);a&&"audio"===r==("audio"===a.kind)||(t=defaultTrackFor(r));const o=e.duration||5,s=i?Timeline.firstGap(project.clips,t,n??state.time,o):Math.max(0,n??state.time),l=e.name.replace(/\.[^.]+$/,""),c={id:"c_"+uid(),mediaId:e.id,kind:r,track:t,start:s,in:0,duration:o,name:l,props:{...DEFAULT_PROPS}};if(project.clips.push(c),"video"===r){c.props.volume=0,ensureAudioTrackCount(2);const t="lg_"+uid();c.linkGroup=t;const n={id:"c_"+uid(),mediaId:e.id,kind:"audio",track:"A1",start:s,in:0,duration:o,name:l,props:{...DEFAULT_PROPS,audioChannel:0},linkGroup:t},i={id:"c_"+uid(),mediaId:e.id,kind:"audio",track:"A2",start:s,in:0,duration:o,name:l,props:{...DEFAULT_PROPS,audioChannel:1},linkGroup:t};project.clips.push(n,i),reconcileAudioChannels(c)}return ensureWave(e),selectClip(c.id),scheduleSave(),c}async function detectChannelCount(e){if(null!=e.channels)return e.channels;try{const t=await fetch(projectApi("/api/audio-info?src="+encodeURIComponent(e.src)),{cache:"no-store"}),n=await t.json();if(t.ok&&Number.isInteger(n.channels))return null==e.channels&&(e.channels=n.channels),e.channels}catch{}try{if(!browserDecodable(e))throw new Error("source too long to decode in the browser");const t=await getAudioBuffer(e);null==e.channels&&(e.channels=t.numberOfChannels)}catch{null==e.channels&&(e.channels=2)}return e.channels}async function reconcileAudioChannels(e){if("video"!==e.kind||!e.linkGroup)return;const t=e.mediaId,n=getMedia(t);if(!n)return;const i=await detectChannelCount(n);state.gesture&&await new Promise(e=>window.addEventListener("timelinegestureend",e,{once:!0}));const r=getClip(e.id);if(!r||r.mediaId!==t)return;const a=r.linkGroup,o=AUDIO_TRACK_IDS.length;i>o&&ensureAudioTrackCount(i);const s=AUDIO_TRACK_IDS.length-o,l=Math.min(i,16),c=project.clips.filter(e=>e.linkGroup===a&&"audio"===e.kind);let d=0,u=0;for(const e of c)(e.props?.audioChannel??0)>=l&&(releaseClipEl(e.id),project.clips=project.clips.filter(t=>t!==e),u++);for(let e=0;e<l;e++)c.some(t=>t.props?.audioChannel===e)||(project.clips.push({id:"c_"+uid(),mediaId:t,kind:"audio",track:"A"+(e+1),start:r.start,in:r.in,duration:r.duration,name:r.name,props:{...DEFAULT_PROPS,audioChannel:e},linkGroup:a}),d++);(d||u)&&(state.dirtyTimeline=!0,scheduleSave(),renderInspector(),i>16?toast(uiText("{name}: {count} audio channels, only {max} channels supported — extra channels dropped",{name:n.name,count:i,max:16})):d?toast(s?uiText("{name}: added {tracks} audio tracks and linked {channels} channels",{name:n.name,tracks:s,channels:l}):uiText("Linked {count} audio channels from {name}",{count:l,name:n.name})):u&&toast(uiText("{name} has fewer channels — removed {count} linked audio clips",{name:n.name,count:u})))}function applyTitleStyle(e,t,{keepTransform:n=!1}={}){const i=TITLE_STYLES[t]||TITLE_STYLES.plain,r=e.props,a=n?{x:r.x,y:r.y,scale:r.scale,rotation:r.rotation,align:r.align}:null;if(Object.assign(r,STYLE_RESET,i.props),a)Object.assign(r,a);else{const e=project.height||720,t=project.width||1280,n=i.place||"center";r.x=0,r.y="lower"===n?Math.round(.3*e):"upper"===n?-Math.round(.3*e):"lower-left"===n?Math.round(.28*e):0,"lower-left"===n&&(r.x=-Math.round(.18*t),r.align="left")}e.styleName=t}function openStylePicker(e,t){const n={props:JSON.parse(JSON.stringify(t.props)),styleName:t.styleName};let i=!1;const r=()=>{i||(t.props=JSON.parse(JSON.stringify(n.props)),t.styleName=n.styleName)},a=document.createElement("div");a.className="style-menu";for(const[e,n]of Object.entries(TITLE_STYLES)){const o=document.createElement("div");o.className="style-opt"+(t.styleName===e?" on":""),o.textContent=uiText(n.label),o.style.fontFamily=`"${n.props.font}", sans-serif`,n.props.uppercase&&(o.style.textTransform="uppercase"),o.addEventListener("mouseenter",()=>{r(),applyTitleStyle(t,e,{keepTransform:!0})}),o.addEventListener("click",()=>{r(),pushUndo(),applyTitleStyle(t,e,{keepTransform:!0}),i=!0,s(),scheduleSave(),renderInspector()}),a.appendChild(o)}a.addEventListener("mouseleave",r);const o=t=>{a.contains(t.target)||t.target===e||s()};function s(){r(),a.remove(),document.removeEventListener("pointerdown",o,!0),runtime.styleMenu=null}document.addEventListener("pointerdown",o,!0);const l=e.getBoundingClientRect();a.style.left=Math.round(Math.min(l.left,innerWidth-220))+"px",a.style.top=Math.round(Math.min(l.bottom+4,innerHeight-340))+"px",document.body.appendChild(a),runtime.styleMenu={close:s}}function openFontPicker(e,t){closeFontPicker();const n=document.createElement("div");n.className="style-menu font-menu",n.id="font-picker-menu",n.tabIndex=-1,n.setAttribute("role","listbox"),n.setAttribute("aria-label",uiText("Family"));const i=[],r=new Set,a=(e,a)=>{const o=a.filter(e=>!r.has(e)&&r.add(e));if(!o.length)return;const s=document.createElement("div");s.setAttribute("role","group"),s.setAttribute("aria-label",uiText(e));const c=document.createElement("div");c.className="font-group-label",c.textContent=uiText(e),c.setAttribute("aria-hidden","true"),s.appendChild(c);for(const e of o){const n=document.createElement("div");n.className="style-opt font-opt"+(e===t.props.font?" on":""),n.id=`font-picker-option-${i.length}`,n.setAttribute("role","option"),n.setAttribute("aria-selected",String(e===t.props.font)),n.dataset.font=e,n.textContent=n.title=e,n.style.fontFamily=`${JSON.stringify(e)}, sans-serif`,n.addEventListener("click",()=>l(e)),i.push(n),s.appendChild(n)}n.appendChild(s)};a("Library fonts",runtime.customFonts),a("System",SYSTEM_FONTS),a("Family",[t.props.font]);let o=Math.max(0,i.findIndex(e=>e.dataset.font===t.props.font));function s(e){i[o]?.classList.remove("active"),o=clamp(e,0,i.length-1);const t=i[o];t.classList.add("active"),n.setAttribute("aria-activedescendant",t.id),t.scrollIntoView({block:"nearest"})}function l(e){e!==t.props.font&&(pushUndo(),t.props.font=e,state.dirtyTimeline=!0,scheduleSave()),c(),renderInspector(),els.inspector.querySelector("[data-font-open]")?.focus({preventScroll:!0})}function c(){n.remove(),e.setAttribute("aria-expanded","false"),e.removeAttribute("aria-controls"),document.removeEventListener("pointerdown",d,!0),document.removeEventListener("scroll",u,!0),window.removeEventListener("resize",c),runtime.fontMenu=null}const d=t=>{n.contains(t.target)||e.contains(t.target)||c()},u=e=>{n.contains(e.target)||c()};n.addEventListener("keydown",t=>{if(t.stopPropagation(),"Tab"===t.key)return c(),void e.focus({preventScroll:!0});if(t.preventDefault(),"Escape"===t.key)c(),e.focus({preventScroll:!0});else if("ArrowDown"===t.key)s(o+1);else if("ArrowUp"===t.key)s(o-1);else if("Home"===t.key)s(0);else if("End"===t.key)s(i.length-1);else if("Enter"===t.key||" "===t.key)l(i[o].dataset.font);else if(1===t.key.length&&!t.ctrlKey&&!t.metaKey&&!t.altKey){const e=i.map((e,t)=>(o+t+1)%i.length).find(e=>i[e].dataset.font.toLowerCase().startsWith(t.key.toLowerCase()));void 0!==e&&s(e)}}),document.body.appendChild(n);const p=e.getBoundingClientRect();n.style.width=Math.min(Math.max(p.width,220),innerWidth-16)+"px",n.style.maxHeight=Math.min(330,innerHeight-16)+"px",n.style.left=clamp(p.left,8,Math.max(8,innerWidth-n.offsetWidth-8))+"px",n.style.top=clamp(p.bottom+4,8,Math.max(8,innerHeight-n.offsetHeight-8))+"px",e.setAttribute("aria-expanded","true"),e.setAttribute("aria-controls",n.id),runtime.fontMenu={close:c},n.focus({preventScroll:!0}),s(o),document.addEventListener("pointerdown",d,!0),document.addEventListener("scroll",u,!0),window.addEventListener("resize",c)}function closeFontPicker(){runtime.fontMenu?.close()}function closeStylePicker(){runtime.styleMenu&&runtime.styleMenu.close()}function replaceClipMedia(e,t){if(!t||e.mediaId===t.id)return;pushUndo();const n="video"===e.kind&&e.linkGroup?project.clips.filter(t=>t.linkGroup===e.linkGroup):[e];let i=!1;for(const e of n){if(e.mediaId=t.id,releaseClipEl(e.id),null==t.duration)continue;const n=e.props?.speed||1,r=Math.max(0,t.duration-.001);e.in>r&&(e.in=r,i=!0);const a=Math.max(.05,(t.duration-e.in)/n);e.duration>a&&(e.duration=a,i=!0)}"video"!==t.kind&&"audio"!==t.kind||ensureWave(t),"video"===e.kind&&reconcileAudioChannels(e),state.dirtyTimeline=!0,scheduleSave(),renderInspector(),toast(uiText(i?"Media replaced — trimmed to fit shorter source":"Media replaced"))}els.binList.addEventListener("click",e=>{(e.ctrlKey||e.metaKey)&&0===e.button&&(e.target.closest(".bin-del")||(e.preventDefault(),openFileImport(e.clientX,e.clientY)))});let replaceFileInput=null,replaceFileTargetId=null;function pickReplacementFile(e){replaceFileInput||(replaceFileInput=document.createElement("input"),replaceFileInput.type="file",replaceFileInput.accept=els.fileInput.accept,replaceFileInput.className="sr-only",document.body.appendChild(replaceFileInput),replaceFileInput.addEventListener("change",async()=>{const e=replaceFileInput.files;replaceFileInput.value="";const t=replaceFileTargetId;if(replaceFileTargetId=null,!e.length)return;const n=await importFiles(e),i=getClip(t);if(!i||!n.length)return;const r=n.find(e=>e.kind===i.kind)||n[0];r.kind===i.kind?replaceClipMedia(i,r):toast(uiText("Imported, but cannot replace {kind} with {replacement}",{kind:uiText(i.kind),replacement:uiText(r.kind)}))})),replaceFileTargetId=e.id,replaceFileInput.click()}function openMediaPicker(e,t){const n=project.media.filter(e=>e.kind===t.kind&&e.id!==t.mediaId),i=document.createElement("div");i.className="style-menu";const r=document.createElement("div");if(r.className="style-opt media-opt",r.textContent=uiText("📂 Browse file…"),r.addEventListener("click",()=>{o(),pickReplacementFile(t)}),i.appendChild(r),n.length){const e=document.createElement("div");e.style.cssText="height:1px;background:var(--border);margin:4px 2px;",i.appendChild(e);for(const e of n){const n=document.createElement("div");n.className="style-opt media-opt",n.textContent=e.name,n.title=e.name,n.addEventListener("click",()=>{o(),replaceClipMedia(t,e)}),i.appendChild(n)}}else{const e=document.createElement("div");e.className="style-opt media-opt",e.style.opacity=".6",e.style.cursor="default",e.textContent=uiText("No other {kind} in bin",{kind:uiText(t.kind)}),i.appendChild(e)}const a=t=>{i.contains(t.target)||t.target===e||o()};function o(){i.remove(),document.removeEventListener("pointerdown",a,!0),runtime.mediaMenu=null}document.addEventListener("pointerdown",a,!0);const s=e.getBoundingClientRect();i.style.left=Math.round(Math.min(s.left,innerWidth-220))+"px",i.style.top=Math.round(Math.min(s.bottom+4,innerHeight-340))+"px",document.body.appendChild(i),runtime.mediaMenu={close:o}}function closeMediaPicker(){runtime.mediaMenu&&runtime.mediaMenu.close()}function addTitle(){pushUndo();const e={id:"c_"+uid(),mediaId:null,kind:"text",track:"V2",start:state.time,in:0,duration:4,name:DEFAULT_PROPS.text,props:{...DEFAULT_PROPS}};runtime.titleStyleIdx=((runtime.titleStyleIdx||0)+1)%STYLE_CYCLE.length,applyTitleStyle(e,STYLE_CYCLE[runtime.titleStyleIdx]),e.props.textAnim="none",project.clips.push(e),selectClip(e.id),scheduleSave(),focusTextContent()}function addAdjust(){pushUndo();const e={id:"c_"+uid(),mediaId:null,kind:"adjust",track:"V3",start:state.time,in:0,duration:4,name:"Adjust",props:{...DEFAULT_PROPS}};project.clips.push(e),selectClip(e.id),scheduleSave()}function deleteRippleShifts(e){const t=new Set(e.map(e=>e.id)),n=new Map,i=new Set;for(const r of e)if(!i.has(r.id)){for(const e of withLinked([r]))i.add(e.id);for(const[e]of trimFollowers(r,r))t.has(e)||(n.has(e)||n.set(e,[]),n.get(e).push([r.start,clipEnd(r)]))}const r=new Map,a=new Map,o=new Map;for(const e of project.clips){if(t.has(e.id))continue;let i=0,s=-1/0;for(const[t,r]of(n.get(e.id)||[]).sort((e,t)=>e[0]-t[0]))i+=Math.max(0,r-Math.max(t,s)),s=Math.max(s,r);const l=e.linkGroup?`link:${e.linkGroup}`:`clip:${e.id}`;r.has(l)||r.set(l,{shift:1/0});const c=r.get(l);c.shift=Math.min(c.shift,i,e.start),a.set(e.id,c),o.has(e.track)||o.set(e.track,[]),o.get(e.track).push(e)}for(const e of r.values())e.desiredShift=e.shift;for(const e of o.values())e.sort((e,t)=>e.start-t.start);let s=!0;for(;s;){s=!1;for(const e of o.values())for(let t=1;t<e.length;t++){const n=e[t-1],i=e[t],r=a.get(i.id),o=a.get(n.id),l=Math.min(Math.max(0,i.start-clipEnd(n)),Math.max(0,r.desiredShift-o.desiredShift)),c=o.shift+l;r.shift>c+Timeline.EPS&&(r.shift=c,s=!0);const d=Math.max(0,r.shift-l);o.shift>d+Timeline.EPS&&(o.shift=d,s=!0)}}return new Map([...a].map(([e,t])=>[e,t.shift]))}function deleteSelected(){const e=withLinked(selectedClips());if(!e.length)return;pushUndo();const t=new Set(e.map(e=>e.id)),n=state.snap?deleteRippleShifts(e):new Map;for(const t of e)releaseClipEl(t.id);project.clips=project.clips.filter(e=>!t.has(e.id));for(const e of project.clips)e.start=Math.max(0,e.start-(n.get(e.id)||0));setSelection([]),scheduleSave(),renderInspector()}const GAP_EPS=1e-4;function gapAtPlayhead(e,t){const n=project.clips.filter(t=>t.track===e).sort((e,t)=>e.start-t.start);for(const e of n)if(e.start<=t&&t<clipEnd(e))return null;let i=0;for(const e of n)clipEnd(e)<=t&&(i=Math.max(i,clipEnd(e)));let r=1/0;for(const e of n)e.start>t&&(r=Math.min(r,e.start));return{gapStart:i,gapEnd:r}}function enabledTracks(){return TRACKS.filter(e=>isTrackEnabled(e.id))}function sharedGapAt(e){const t=enabledTracks();if(!t.length)return{reason:uiText("No enabled tracks")};let n=0,i=1/0;for(const r of t){const t=gapAtPlayhead(r.id,e);if(!t)return{reason:uiText("No shared gap: {track} contains a clip",{track:trackLabel(r.id)})};n=Math.max(n,t.gapStart),i=Math.min(i,t.gapEnd)}const r=i-n;if(!isFinite(i)||r<=1e-4)return{reason:uiText("Nothing to close at playhead")};const a=project.clips.filter(e=>isTrackEnabled(e.track)&&e.start>=i-1e-4);return a.length?{L:n,R:i,G:r,movers:a}:{reason:uiText("Nothing to close at playhead")}}function closeGapAtPlayhead(e=state.time){const t=sharedGapAt(e);if(t.reason)return void toast(t.reason);const{G:n,movers:i}=t;pushUndo();for(const e of i)e.start=Math.max(0,e.start-n);state.dirtyTimeline=!0,scheduleSave();const r=n>=1?n.toFixed(2):n.toFixed(3);toast(uiText("Closed {seconds}s gap",{seconds:r}))}function clearFocusedTransition(){if(!state.transFocus||!state.selId)return!1;const e=getClip(state.selId);if(!e)return!1;const t="in"===state.transFocus?"transitionIn":"transitionOut";return!!e[t]&&(pushUndo(),e[t]=void 0,state.transFocus=null,state.dirtyTimeline=!0,scheduleSave(),renderInspector(),!0)}function loadLastTransition(e){try{const t=JSON.parse(localStorage.getItem(LAST_TRANS_KEY[e])||"null");if(t?.type&&"none"!==t.type&&TRANSITIONS.includes(t.type)){const e=+t.duration;return{type:t.type,duration:isFinite(e)&&e>=MIN_TRANS_DUR?e:1}}}catch{}return{...DEFAULT_LAST_TRANS}}function saveLastTransition(e,t){if(t?.type&&"none"!==t.type)try{localStorage.setItem(LAST_TRANS_KEY[e],JSON.stringify({type:t.type,duration:Math.max(MIN_TRANS_DUR,+t.duration||1)}))}catch{}}function addTransitionAtPlayhead(){const e=getClip(state.selId);if(!e)return void toast(uiText("Select a clip first"));const t=state.time;if(t<e.start||t>=clipEnd(e))return void toast(uiText("Move playhead over the selected clip"));const n=(t-e.start)/e.duration<.5?"in":"out",i="in"===n?"transitionIn":"transitionOut",r=loadLastTransition(n),a=Math.min(Math.max(MIN_TRANS_DUR,r.duration),e.duration);pushUndo(),e[i]={type:r.type,duration:+a.toFixed(3)},saveLastTransition(n,e[i]),state.dirtyTimeline=!0,selectClip(e.id,{transFocus:n}),scheduleSave()}function gapSearchRange(){return null!=project.inPoint&&null!=project.outPoint?{t0:project.inPoint,t1:project.outPoint}:{t0:0,t1:Math.max(projDur(),0)}}function listAlignedGaps(e,t){const n=enabledTracks();if(!n.length||t-e<=1e-4)return[];const i=new Set([e,t]);for(const n of project.clips){if(!isTrackEnabled(n.track))continue;const r=n.start,a=clipEnd(n);r>e&&r<t&&i.add(r),a>e&&a<t&&i.add(a)}const r=[...i].sort((e,t)=>e-t),a=[],o=new Set;for(let i=0;i<r.length-1;i++){const s=r[i],l=r[i+1];if(l-s<=1e-4)continue;const c=(s+l)/2;let d=-1/0,u=1/0,p=!0;for(const e of n){const t=gapAtPlayhead(e.id,c);if(!t){p=!1;break}d=Math.max(d,t.gapStart),u=Math.min(u,t.gapEnd)}if(!p)continue;if(d=Math.max(d,e),u=Math.min(isFinite(u)?u:t,t),u-d<=1e-4)continue;const m=d.toFixed(5)+":"+u.toFixed(5);o.has(m)||(o.add(m),a.push({L:d,R:u}))}return a.sort((e,t)=>e.L-t.L),a}function ensurePlayheadVisible(){const e=state.time*state.pps,t=els.timelineScroll;t&&(e<t.scrollLeft||e>t.scrollLeft+t.clientWidth-40)&&(t.scrollLeft=Math.max(0,e-t.clientWidth/3))}function goToNextGap(){const{t0:e,t1:t}=gapSearchRange();if(t-e<=1e-4)return void toast(uiText("No gaps found"));const n=listAlignedGaps(e,t);if(!n.length)return void toast(null!=project.inPoint&&null!=project.outPoint?uiText("No gaps in IN/OUT range"):uiText("No gaps found"));const i=state.time;let r=n.find(e=>(e.L+e.R)/2>i+1e-4);r||(r=n[0]),setTime((r.L+r.R)/2),ensurePlayheadVisible()}function splitAtPlayhead(){const e=state.time;let t=state.selIds.size?selectedClips():project.clips;if(t=withLinked(t.filter(t=>e>t.start+.05&&e<clipEnd(t)-.05)),!t.length)return;pushUndo();const n=new Map;for(const i of t){const t=splitClipAt(i,e);t&&n.set(i.id,t)}relinkSplitRights(t,n),scheduleSave()}function splitClipAt(e,t){if(!(t>e.start+.05&&t<clipEnd(e)-.05))return null;const n=t-e.start,i={...e,id:"c_"+uid(),props:{...e.props},start:t,in:e.in+n*clipSpeed(e),duration:clipEnd(e)-t,keyframes:shiftKF(e.keyframes,n,clipEnd(e)-t),transitionIn:void 0,linkedId:void 0,linkGroup:void 0};return e.duration=n,e.keyframes=shiftKF(e.keyframes,0,n),e.transitionOut=void 0,project.clips.push(i),i}function relinkSplitRights(e,t){const n=new Map;for(const i of e){const e=t.get(i.id);if(e)if(i.linkGroup)n.has(i.linkGroup)||n.set(i.linkGroup,"lg_"+uid()),e.linkGroup=n.get(i.linkGroup);else{const n=i.linkedId?t.get(i.linkedId):null;n?(e.linkedId=n.id,n.linkedId=e.id):delete e.linkedId}}}function splitAtWorkArea(){const e=[project.inPoint,project.outPoint].filter(e=>null!=e);if(!e.length)return void toast(uiText("Set an IN or OUT marker first (I / O)"));const t=withLinked(project.clips.filter(t=>(e=>"function"!=typeof isTrackEnabled||isTrackEnabled(e.track))(t)&&e.some(e=>e>t.start+.05&&e<clipEnd(t)-.05)));if(t.length){pushUndo();for(const n of e.slice().sort((e,t)=>t-e)){const e=t.filter(e=>n>e.start+.05&&n<clipEnd(e)-.05),i=new Map;for(const t of e){const e=splitClipAt(t,n);e&&i.set(t.id,e)}relinkSplitRights(e,i)}scheduleSave()}else toast(uiText("Nothing to split at IN/OUT"))}function trimToPlayhead(e){const t=getClip(state.selId);if(!t)return;const n=state.time,i="in"===e&&n>t.start&&n<clipEnd(t)-.05,r="out"===e&&n>t.start+.05&&n<clipEnd(t);if(!i&&!r)return;const a={start:t.start,duration:t.duration};if(pushUndo(),i){const e=n-t.start;t.start=n,t.in="video"===t.kind||"audio"===t.kind?t.in+e*clipSpeed(t):0,t.duration-=e}else t.duration=n-t.start;for(const e of withLinked([t]))e.keyframes=shiftKF(e.keyframes,i?n-a.start:0,t.duration);syncLinkedTiming(t),rippleTrim(t,a),scheduleSave(),renderInspector()}function trimFollowers(e,t){const n=withLinked([e]),i=new Set(n.map(e=>e.id)),r=t.start+t.duration,a=new Map(n.map(e=>[e.track,r])),o=new Map;for(;;){const e=project.clips.filter(e=>!i.has(e.id)&&!o.has(e.id)&&a.has(e.track)&&e.start>=a.get(e.track)-1e-9);if(!e.length)return o;for(const t of withLinked(e))i.has(t.id)||o.has(t.id)||(o.set(t.id,t.start),a.set(t.track,Math.min(a.get(t.track)??1/0,t.start)))}}function rippleTrim(e,t,n=trimFollowers(e,t)){const i=e.duration-t.duration;e.start=t.start,syncLinkedTiming(e);for(const[e,t]of n){const n=getClip(e);n&&(n.start=Math.max(0,t+i))}}function trimToWorkArea(){const e=project.inPoint,t=project.outPoint;if(null==e&&null==t)return void toast(uiText("Set an IN or OUT marker first (I / O)"));const n=e=>"function"!=typeof isTrackEnabled||isTrackEnabled(e.track);if(!project.clips.some(i=>{if(!n(i))return!1;const r=i.start,a=clipEnd(i);let o=r,s=a;return null!=e&&(o=Math.max(o,e)),null!=t&&(s=Math.min(s,t)),s-o<.05||o>r+1e-6||s<a-1e-6}))return void toast(uiText("Nothing to trim"));pushUndo();const i=new Set;for(const r of project.clips){if(!n(r))continue;const a=r.start;let o=a,s=clipEnd(r);if(null!=e&&(o=Math.max(o,e)),null!=t&&(s=Math.min(s,t)),s-o<.05){i.add(r.id);continue}const l=o-a;l>1e-6&&(r.start=o,"video"===r.kind||"audio"===r.kind?r.in+=l*clipSpeed(r):r.in=0,r.duration-=l,r.keyframes=shiftKF(r.keyframes,l,r.duration),r.transitionIn=void 0),clipEnd(r)-s>1e-6&&(r.duration=Math.max(.05,s-r.start),r.keyframes=shiftKF(r.keyframes,0,r.duration),r.transitionOut=void 0)}for(const e of i)releaseClipEl(e);i.size&&(project.clips=project.clips.filter(e=>!i.has(e.id))),pruneSelection(),scheduleSave(),renderInspector()}function hasWorkArea(){return null!=project.inPoint||null!=project.outPoint}function playRange(){const e=null!=project.inPoint?project.inPoint:0,t=null!=project.outPoint?project.outPoint:Math.max(projDur(),0);return{start:e,end:Math.max(t,e)}}function playLimited(){return state.workAreaPlay&&!state.exporting&&hasWorkArea()}function playStopAt(e){const t=Math.max(e??projDur(),0);if(!playLimited())return t;const{end:n}=playRange();return state.time>n+1e-4?t:n}function gotoHome(){setTime(playLimited()&&null!=project.inPoint?project.inPoint:0)}function gotoEnd(){setTime(playLimited()&&null!=project.outPoint?project.outPoint:projDur())}function syncTrimIOButton(){const e=hasWorkArea(),t=$("btnTrimIO"),n=$("btnWorkAreaPlay");t&&t.classList.toggle("hidden",!e),n&&(n.classList.toggle("hidden",!e),n.classList.toggle("on",state.workAreaPlay))}function trackLabel(e){const t=TRACKS.find(t=>t.id===e)?.name;return t||uiText(e.startsWith("A")?"Audio {number}":"Video {number}",{number:e.slice(1)})}function trackIconKind(e){if(e.startsWith("A"))return"audio";const t=project.clips.filter(t=>t.track===e);return t.length&&t.every(e=>"text"===e.kind)?"text":"video"}function trackToggleIcon(e){return`<svg class="track-ico" data-kind="${e}" viewBox="0 0 16 16" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round">${"text"===e?'<path d="M1 4h6M4 4v9M9 4h6M12 4v9"/>':"audio"===e?'<circle cx="8" cy="8" r="6.4"/><path d="M9.5 4.3v6.1M9.5 4.3l2 .8M9.5 10.4c0 1.7-3.5 2.2-3.5.4 0-1.6 3.5-2.1 3.5-.4Z"/>':'<rect x="1.5" y="3" width="13" height="10" rx=".7"/><path d="m6.5 5.5 4 2.5-4 2.5Z" fill="currentColor" stroke="none"/>'}<path class="track-ico-off" d="M2 2l12 12"/></svg>`}function timelineVideoLabels(){const e=new Map(TRACKS.map((e,t)=>[e.id,t])),t=e=>state.clipTrimOrigins?.get(e.id)??e.start;return new Map(project.clips.filter(t=>"video"===t.kind&&e.has(t.track)).sort((n,i)=>t(n)-t(i)||e.get(n.track)-e.get(i.track)||n.id.localeCompare(i.id)).map((e,t)=>[e.id,uiText("Clip {number}",{number:t+1})]))}function buildTrackDOM(){els.tracks.dataset.signature=visibleTrackSignature(),els.trackHeaders.innerHTML="",els.tracks.innerHTML="";const e=document.createElement("div");e.id="trackHeadInner",els.trackHeaders.appendChild(e);for(const t of visibleTracks()){const n=isTrackEnabled(t.id),i=document.createElement("div");i.className="track-head"+(n?"":" disabled"),i.dataset.track=t.id,i.style.height=t.h+"px";const r=escapeHtml(trackLabel(t.id));i.innerHTML=`<button type="button" class="track-toggle" aria-pressed="${n}" title="${uiAttr(trackLabel(t.id))}" aria-label="${uiAttr(trackLabel(t.id))}">${trackToggleIcon(trackIconKind(t.id))}</button><span class="track-id sr-only">${r}</span>`,i.querySelector(".track-toggle").addEventListener("click",e=>{e.preventDefault(),e.stopPropagation(),toggleTrackEnabled(t.id)}),e.appendChild(i);const a=document.createElement("div");a.className="track"+(n?"":" disabled"),a.dataset.track=t.id,a.style.height=t.h+"px",els.tracks.appendChild(a)}if(!visibleTracks().length){const e=document.createElement("div");e.className="timeline-empty",e.textContent=uiText("Drop media here to create a track"),els.tracks.appendChild(e)}updateTimelineTrackLayout()}function updateTimelineTrackLayout(){if(state.gesture)return;const e=Math.max(0,els.timelineScroll.clientHeight-26),t=visibleTracks(),n=TRACK_SIZE_PRESETS[state.trackSize]||TRACK_SIZE_PRESETS.l,i=t.map(e=>n.h[e.id]??n.h["audio"===e.kind?"A1":"V1"]),r=i.reduce((e,t)=>e+t,0),a=r?Math.min(1,e/r):1,o=r?Math.max(0,(e-r*a)/2):0;t.forEach((e,t)=>{e.h=i[t]*a;const n=els.tracks.querySelector(`[data-track="${e.id}"]`),r=els.trackHeaders.querySelector(`[data-track="${e.id}"]`);for(const t of[n,r])t&&(t.style.height=e.h+"px")}),els.tracks.style.marginTop=o+"px";const s=$("trackHeadInner");s&&(s.style.transform=`translateY(${o}px)`)}function scrollTimeline(){els.timelineScroll.scrollLeft=0,els.timelineScroll.scrollTop=0,runtime.timelineScrollLeft=0}function contentWidth(){const e=Math.max(projDur()*state.pps+8,els.timelineScroll.clientWidth||800);return null!=state.clipGestureWidth?state.clipGestureWidth=Math.max(state.clipGestureWidth,e):e}function clipTransitionDur(e){if(!e||"none"===e.type)return 0;const t=+e.duration;return isFinite(t)&&t>0?t:0}function clipBorderRadius(){return"s"===state.trackSize?2:5}function transitionMarksHtml(e,t){let n="";const i=Math.max(8,t-6),r=clipBorderRadius(),a=(t,a)=>{const o=clipTransitionDur(t);if(!o)return;const s=Math.max(4,Math.min(o,e.duration)*state.pps),l=i-r,c=state.selId===e.id&&state.transFocus===a?" focused":"";n+="in"===a?`<div class="trans-mark in${c}" style="width:${s}px" data-side="in"><svg viewBox="0 0 ${s} ${i}" preserveAspectRatio="none" aria-hidden="true"><polygon points="0,0 ${s},0 0,${l}"/></svg><div class="trans-dur-handle" title="${uiAttr("Drag to adjust duration")}"></div></div>`:`<div class="trans-mark out${c}" style="width:${s}px" data-side="out"><svg viewBox="0 0 ${s} ${i}" preserveAspectRatio="none" aria-hidden="true"><polygon points="0,0 ${s},0 ${s},${l}"/></svg><div class="trans-dur-handle" title="${uiAttr("Drag to adjust duration")}"></div></div>`};return a(e.transitionIn,"in"),a(e.transitionOut,"out"),n}function clipKeyframeGroups(e){if(!e?.keyframes)return[];const t=new Map;for(const[n,i]of Object.entries(e.keyframes))if(Array.isArray(i))for(const e of i){const i=+e.t;if(!Number.isFinite(i))continue;const r=i.toFixed(4);let a=t.get(r);a||(a={t:i,keys:[]},t.set(r,a)),a.keys.includes(n)||a.keys.push(n)}return[...t.values()].sort((e,t)=>e.t-t.t)}const clipKeyframeLocalTimes=e=>clipKeyframeGroups(e).map(e=>e.t);function clipKeyframesHtml(e){const t=clipKeyframeGroups(e);if(!(t.length&&e.duration>0))return"";let n='<div class="clip-kfs">';for(const{t:i,keys:r}of t){if(i<-1e-6||i>e.duration+1e-6)continue;const t=Math.max(0,Math.min(100,i/e.duration*100)),a=r.map(e=>uiText(e[0].toUpperCase()+e.slice(1))).join(", ")+" @ "+fmt(e.start+i),o=r.length>1?`<span class="clip-kf-n">${r.length}</span>`:"";n+=`<div class="clip-kf${r.length>1?" multi":""}" style="left:${t}%" data-t="${i}" title="${a}">${o}</div>`}return n+"</div>"}function rebuildClips(){updateTimelineTrackLayout(),updateTimelineZoomControls(),visibleThumbKeys=new Set,neededThumbKeys=new Set,state.time>projDur()&&setTime(projDur()),els.playhead.style.left=state.time*state.pps+"px";const e=timelineVideoLabels(),t=contentWidth();els.tracksContent.style.width=t+"px",els.ruler.style.width=els.timelineScroll.clientWidth+"px";const n=state.gesture&&runtime.dropClipPreview,i=new Map(n?[...els.tracks.querySelectorAll(".clip")].map(e=>[e.dataset.id,e]):[]);if(!n)for(const e of els.tracks.children)e.dataset.track&&(e.innerHTML="");for(const t of project.clips){const n=trackOf(t);if(!n)continue;const r=els.tracks.querySelector(`[data-track="${t.track}"]`),a=i.get(t.id)||document.createElement("div");i.delete(t.id);const o=["drag-origin","reorder-preview"].filter(e=>a.classList.contains(e)),s=clipMediaFailure(t);a.className=`clip c-${t.kind}`+(s?" media-missing":"")+(state.selIds.has(t.id)?" selected":"")+(t.id===state.selId?" primary":"")+(o.length?" "+o.join(" "):""),a.dataset.id=t.id,a.title=s?mediaFailureLabel(s)+" · "+(getMedia(t.mediaId)?.name||t.name||""):t.name||"",a.style.left=t.start*state.pps+"px",a.style.width=Math.max(8,t.duration*state.pps)+"px";let l="";!s&&"video"===t.kind&&trackSizeShowsThumbs()&&r.offsetTop+n.h>=els.timelineScroll.scrollTop&&r.offsetTop<=els.timelineScroll.scrollTop+els.timelineScroll.clientHeight&&(l+=clipThumbStrip(t,n.h));const c=!s&&"audio"===t.kind&&!!wavePeaksFor(t);c&&(l+='<canvas class="wave"></canvas>');const d=(t.keyframes&&Object.keys(t.keyframes).length?"◆ ":"")+(t.transitionIn||t.transitionOut?"⇄ ":""),u=t.props?.audioChannel,p=0===u?"L · ":1===u?"R · ":Number.isInteger(u)?`Ch${u+1} · `:"",m="text"===t.kind?"T · "+(t.props.text||"").split("\n")[0]:"adjust"===t.kind?"FX · "+(t.name||""):"audio"===t.kind?p+(t.name||""):"video"===t.kind?e.get(t.id):t.name||"";l+=`<div class="fade"></div>\n <div class="clip-label">${s?"⚠ "+mediaFailureLabel(s)+" · ":""}${d}${escapeHtml(m)}</div>`,l+=clipKeyframesHtml(t);let f=`<div class="clip-body">${l}</div>`;f+=transitionMarksHtml(t,n.h),f+='<div class="handle l"></div><div class="handle r"></div>',a.innerHTML=f,c&&a.classList.add("has-wave"),a.parentElement!==r&&r.appendChild(a),c&&drawClipWave(a.querySelector(".wave"),t,n.h)}for(const e of i.values())e.remove();applyDropClipPreview(),preloadClipThumbs(),$("coverDialog").open&&renderCoverTimeline(),pruneClipThumbs(),loadClipThumbs(),state.dirtyTimeline=!1,updateWorkArea()}function drawClipWave(e,t,n){const i=wavePeaksFor(t);if(!(i instanceof Float32Array))return;const r=Math.min(2400,Math.max(8,Math.round(t.duration*state.pps))),a=n-8;e.width=r,e.height=a;const o=e.getContext("2d");o.fillStyle="#c9f29b",o.globalAlpha=.75;const s=a/2;for(let e=0;e<r;e++){const n=mediaTimeAt(t,t.start+e/r*t.duration),l=i[Math.floor(50*n)]||0,c=Math.max(1,l*(a-2));o.fillRect(e,s-c/2,1,c)}}let rulerWorker=null,rulerWorkerTried=!1;function ensureRulerWorker(){if(!rulerWorkerTried){rulerWorkerTried=!0;try{if(window.Worker&&els.ruler.transferControlToOffscreen){const e=els.ruler.transferControlToOffscreen(),t=new Worker("ruler-worker.js");t.postMessage({type:"init",canvas:e},[e]),rulerWorker=t}}catch{rulerWorker=null}}}function drawRuler(){ensureRulerWorker();const e=window.devicePixelRatio||1,t=els.timelineScroll.clientWidth;els.ruler.style.width=t+"px",els.ruler.style.height="26px",rulerWorker?rulerWorker.postMessage({type:"draw",w:t,h:26,dpr:e,sl:els.timelineScroll.scrollLeft,pps:state.pps,markers:project.markers,inPoint:project.inPoint,outPoint:project.outPoint,time:state.time,fps:project.fps,duration:projDur()}):drawRulerMainThread(t,26,e)}function drawRulerMainThread(e,t,n,i=els.ruler,r=els.timelineScroll.scrollLeft,a=state.pps,o=state.time,s=!0){i.width===e*n&&i.height===t*n||(i.width=e*n,i.height=t*n);const l=i.getContext("2d");l.setTransform(n,0,0,n,0,0),l.clearRect(0,0,e,t);const c=([.1,.25,.5,1,2,5,10,15,30,60,120,300].find(e=>e*a>=70)||600)/5,d=Math.max(0,Math.floor(r/a/c));l.strokeStyle="#4a4a55",l.fillStyle="#9a9aa6",l.font="10px Consolas, monospace",l.beginPath();const u=projDur();for(let n=d;n*c*a<r+e&&n*c<=u;n++){const e=n*c,i=Math.round(e*a-r)+.5,o=n%5==0;l.moveTo(i,o?8:17),l.lineTo(i,t),o&&l.fillText(fmt(Math.round(1e3*e)/1e3).slice(0,5),i+4,12)}l.stroke();const p=s?project.inPoint:null,m=s?project.outPoint:null;if(null!=p&&null!=m&&m>p){const n=p*a-r,i=m*a-r;l.fillStyle="#00000055",n>0&&l.fillRect(0,0,Math.min(e,n),t),i<e&&l.fillRect(Math.max(0,i),0,e-Math.max(0,i),t)}for(const n of(s?project.markers:[])||[]){const i=n.t*a-r;i<-6||i>e+6||(l.fillStyle="#ffd166",l.beginPath(),l.moveTo(i,t-9),l.lineTo(i+4,t-5),l.lineTo(i,t-1),l.lineTo(i-4,t-5),l.closePath(),l.fill())}const f=t-1,h=f-.75*(t-4),g=(h+f)/2;if(l.globalCompositeOperation="difference",null!=p){const t=p*a-r;t>=-10&&t<=e+10&&(l.fillStyle="#5eead4",l.beginPath(),l.moveTo(t,h),l.lineTo(t,f),l.lineTo(t+8,g),l.closePath(),l.fill())}if(null!=m){const t=m*a-r;t>=-10&&t<=e+10&&(l.fillStyle="#fb923c",l.beginPath(),l.moveTo(t,h),l.lineTo(t,f),l.lineTo(t-8,g),l.closePath(),l.fill())}l.globalCompositeOperation="source-over";const v=o*a-r;v>=-8&&v<=e+8&&(l.fillStyle="#ff4d6a",l.beginPath(),l.moveTo(v-6,12),l.lineTo(v+6,12),l.lineTo(v+6,19),l.lineTo(v,25),l.lineTo(v-6,19),l.closePath(),l.fill())}function snapTime(e,t,n=project.clips){if(!state.snap)return e;const i=t instanceof Set?t:new Set(t?[t]:[]),r=8/state.pps,a=[0,state.time];for(const e of project.markers||[])a.push(e.t);null!=project.inPoint&&a.push(project.inPoint),null!=project.outPoint&&a.push(project.outPoint);for(const e of n)i.has(e.id)||a.push(e.start,clipEnd(e));let o=e,s=r;for(const t of a){const n=Math.abs(t-e);n<s&&(s=n,o=t)}return o}function timeAtEvent(e){const t=els.timelineScroll.getBoundingClientRect();return clamp((e.clientX-t.left+els.timelineScroll.scrollLeft)/state.pps,0,1e6)}function trackAtEvent(e){for(const t of els.tracks.children){if(!t.dataset.track)continue;const n=t.getBoundingClientRect();if(e.clientY>=n.top&&e.clientY<n.bottom)return t.dataset.track}return null}function timelineClipHit(e){const t=e.target.closest(".clip");if(!t||e.target.closest(".clip-kf, .trans-mark"))return null;let n=getClip(t.dataset.id);if(!n)return null;const i=timeAtEvent(e);(i<n.start||i>=clipEnd(n))&&(n=project.clips.find(e=>e.track===n.track&&i>=e.start&&i<clipEnd(e))||n);const r=getClip(state.selId);if(!e.ctrlKey&&!e.metaKey&&!e.shiftKey&&r&&state.selIds.has(r.id)&&r.id!==n.id&&r.track===n.track){const e=Math.min(4,r.duration*state.pps/4,n.duration*state.pps/4),t=(i-clipEnd(r))*state.pps,a=(r.start-i)*state.pps;if(Math.abs(n.start-clipEnd(r))<Timeline.EPS&&t>=0&&t<=e)return{clip:r,mode:"trim-r"};if(Math.abs(clipEnd(n)-r.start)<Timeline.EPS&&a>=0&&a<=e)return{clip:r,mode:"trim-l"}}const a=(i-n.start)*state.pps,o=n.duration*state.pps,s=Math.min(8,o/4);return{clip:n,mode:a<=s?"trim-l":o-a<=s?"trim-r":"move"}}function showTrimHover(e){const t=e&&"move"!==e.mode?els.tracks.querySelector(`.clip[data-id="${CSS.escape(e.clip.id)}"] .handle.${"trim-l"===e.mode?"l":"r"}`):null,n=els.tracks.querySelector(".handle.trim-hover");n!==t&&(n?.classList.remove("trim-hover"),t?.classList.add("trim-hover"))}els.tracksContent.addEventListener("pointermove",e=>{state.gesture||showTrimHover(timelineClipHit(e))}),els.tracksContent.addEventListener("pointerleave",()=>showTrimHover(null)),els.timelineScroll.addEventListener("scroll",()=>showTrimHover(null)),window.addEventListener("blur",()=>showTrimHover(null));for(const e of["keydown","keyup"])window.addEventListener(e,e=>{["Control","Meta","Shift"].includes(e.key)&&showTrimHover(null)});els.tracksContent.addEventListener("pointerdown",e=>{if(0!==e.button)return;showTrimHover(null),isTypingTarget(document.activeElement)&&els.inspector.contains(document.activeElement)&&document.activeElement.blur();const t=e.target.closest(".clip");if(!t)return void startMarquee(e);let n=getClip(t.dataset.id);if(!n)return;const i=e.target.closest(".clip-kf");if(i)return e.preventDefault(),selectClip(n.id),void setTime(n.start+(+i.dataset.t||0));const r=e.target.closest(".trans-dur-handle");if(r){const t=r.closest(".trans-mark");return void startTransDurGesture(e,n,t?.classList.contains("out")?"out":"in")}const a=e.target.closest(".trans-mark");if(a)return e.preventDefault(),void selectClip(n.id,{transFocus:a.classList.contains("out")?"out":"in"});const o=timelineClipHit(e);if(!o)return;n=o.clip;const s=e.ctrlKey||e.metaKey||e.shiftKey;if(s){if(selectClip(n.id,{toggle:!0}),!state.selIds.has(n.id))return}else state.selIds.has(n.id)?state.selId!==n.id&&(state.selId=n.id,state.dirtyTimeline=!0,renderInspector()):selectClip(n.id);s||"move"!==o.mode||(setTime(n.start),runtime.followPlayhead=!0,lastTs=null),startClipGesture(e,n,o.mode,!s&&state.selIds.size>1)});const MIN_TRANS_DUR=.1;function startTransDurGesture(e,t,n){e.preventDefault();const i="in"===n?"transitionIn":"transitionOut",r=t[i];if(!r)return;selectClip(t.id,{transFocus:n}),state.gesture=!0;const a=r.duration,o=e.clientX;let s=!1;pushUndo();const l=e=>{const i=e.clientX-o;if(Math.abs(i)<2&&!s)return;s=!0;const l=clamp(a+("in"===n?1:-1)*i/state.pps,MIN_TRANS_DUR,t.duration);r.duration=+l.toFixed(3),state.dirtyTimeline=!0,rebuildClips();const c="in"===n?"transInDur":"transOutDur",d=els.inspector.querySelector(`[data-k="${c}"]`);d&&(d.value=r.duration)},c=()=>{window.removeEventListener("pointermove",l),window.removeEventListener("pointerup",c),state.gesture=!1,window.dispatchEvent(new Event("timelinegestureend")),s&&(scheduleSave(),saveLastTransition(n,t[i])),renderInspector()};window.addEventListener("pointermove",l),window.addEventListener("pointerup",c)}function timelineDropTarget(e,t){const n=[...els.tracks.children].filter(e=>e.dataset.track),i=n.filter(e=>TRACKS.find(t=>t.id===e.dataset.track)?.kind===t),r=els.tracksContent.getBoundingClientRect(),a=runtime.dropPreviewTarget;let o=e.clientY;if(a?.newTrack){const e=r.top+a.top;if(o>=e&&o<e+a.h)return a;o>=e+a.h&&(o-=a.h)}const s=e=>({top:r.top+e.offsetTop,bottom:r.top+e.offsetTop+e.offsetHeight,height:e.offsetHeight}),l=(TRACK_SIZE_PRESETS[state.trackSize]||TRACK_SIZE_PRESETS.s).h["audio"===t?"A1":"V1"];if(!i.length){const e=n.find(e=>"A"===e.dataset.track[0]),i=n.length?"video"===t&&e?e.offsetTop:(n[0]?.offsetTop||0)+els.tracks.offsetHeight:0;return{newTrack:!0,beforeId:"video"===t?e?.dataset.track:void 0,top:i,h:l}}for(const e of i){const t=s(e);if(Math.abs(o-t.top)<=5)return{newTrack:!0,beforeId:e.dataset.track,top:t.top-r.top,h:l};if(Math.abs(o-t.bottom)<=5){const i=n[n.indexOf(e)+1];return{newTrack:!0,beforeId:i?.dataset.track,top:t.bottom-r.top,h:l}}if(o>t.top&&o<t.bottom)return{trackId:e.dataset.track,top:t.top-r.top,h:t.height}}const c=i[i.length-1];return"audio"===t&&o>s(c).bottom?{newTrack:!0,top:c.offsetTop+c.offsetHeight,h:l}:null}function clearDropPreview(){runtime.dropPreviewTarget=null,runtime.dropClipPreview=null,els.tracks.style.paddingBottom="";for(const e of document.querySelectorAll(".drop-shift"))e.style.transform="",e.classList.remove("drop-shift");for(const e of els.tracksContent.querySelectorAll(".drop-preview"))e.remove();for(const e of els.tracks.querySelectorAll(".drag-origin"))e.classList.remove("drag-origin");for(const e of els.tracks.querySelectorAll(".reorder-preview"))e.style.transform="",e.classList.remove("reorder-preview")}function applyDropClipPreview(){const e=runtime.dropClipPreview;if(e){for(const t of els.tracks.querySelectorAll(".clip")){t.classList.toggle("drag-origin",e.ids.has(t.dataset.id));const n=e.shifts.get(t.dataset.id);(n||t.classList.contains("reorder-preview"))&&(t.classList.add("reorder-preview"),t.style.transform=`translateX(${(n||0)*state.pps}px)`)}for(const t of els.tracksContent.querySelectorAll(".drop-ghost[data-clip-id]")){const n=e.doc?.clips.find(e=>e.id===t.dataset.clipId),i=t.parentElement,r=els.timelineScroll,a=i.offsetTop+i.offsetHeight>=r.scrollTop&&i.offsetTop<=r.scrollTop+r.clientHeight,o=t.querySelector(".drop-thumb-strip"),s=n&&a&&trackSizeShowsThumbs()?clipThumbStrip({...n,start:+t.dataset.previewStart},i.offsetHeight):"";o.innerHTML!==s&&(o.innerHTML=s)}}}function createDropGhost(e,t,n,i){const r=document.createElement("div");if(r.className="drop-ghost",r.style.left=e*state.pps+"px",r.style.width=Math.max(8,t*state.pps)+"px","video"===i?.kind){r.classList.add("c-video"),r.dataset.clipId=i.id;const e=document.createElement("div");e.className="drop-thumb-strip",r.appendChild(e)}const a=document.createElement("span");return a.className="drop-ghost-label",a.textContent=n,r.appendChild(a),r.title=n,r}function showDropPreview(e,t,n,i=new Set,r,a,o){if(!e)return void clearDropPreview();els.tracks.style.paddingBottom="";for(const e of document.querySelectorAll(".drop-shift"))e.style.transform="",e.classList.remove("drop-shift");if(runtime.dropPreviewTarget=e,e.newTrack){els.tracks.style.paddingBottom=e.h+"px";for(const t of els.tracks.children){if(!t.dataset.track||t.offsetTop<e.top-1)continue;const n=els.trackHeaders.querySelector(`[data-track="${t.dataset.track}"]`);for(const i of[t,n])i&&(i.classList.add("drop-shift"),i.style.transform=`translateY(${e.h}px)`)}}const s=Number.isFinite(o),l=s?o-t:0,c=s?timelineVideoLabels():new Map,d=e=>e?c.get(e.id)||e.name||("text"===e.kind?(e.props?.text||"").split("\n")[0]:"")||e.id:"",u=new Map([...els.tracksContent.querySelectorAll(".drop-preview")].map(e=>[e.dataset.previewId,e])),p=(t,n,i,r,a,o,c,d=!1)=>{let p=u.get(t);p||(p=document.createElement("div"),p.dataset.previewId=t,p.appendChild(createDropGhost(n,i,r,a)),els.tracksContent.appendChild(p)),u.delete(t),p.className="drop-preview"+(d?" linked-drop-preview":e.newTrack?" new-track":"")+(s?" moving":""),p.style.top=Math.max(0,o)+"px",p.style.height=c+"px";const m=p.querySelector(".drop-ghost");m.classList.toggle("companion",d),m.style.left=(n+l)*state.pps+"px",m.style.width=Math.max(8,i*state.pps)+"px",m.dataset.previewStart=n+l;const f=m.querySelector(".drop-ghost-label");if(f.textContent!==r&&(f.textContent=r),s?m.removeAttribute("title"):m.title=r,s){let e=p.querySelector(".drop-slot");e||(e=document.createElement("div"),e.className="drop-slot",p.prepend(e)),e.style.left=n*state.pps+"px",e.style.width=Math.max(8,i*state.pps)+"px"}return p},m=r?.clips.find(e=>e.id===a),f=p("primary",t,n,s?d(m):dropPlacementLabel(e,t),m,e.top,e.h);if(s){let n=f.querySelector(".drop-insertion");n||(n=document.createElement("div"),n.className="drop-insertion",f.appendChild(n));const a=(r?.clips||[]).filter(t=>i.has(t.id)&&t.track===e.trackId);n.style.left=Math.min(t,...a.map(e=>e.start))*state.pps+"px"}if(r)for(const t of r.clips){if(!i.has(t.id)||t.id===a)continue;const n=els.tracks.querySelector(`[data-track="${t.track}"]`);n&&p("linked:"+t.id,t.start,t.duration,s?d(t):uiText("Moves together"),t,n.offsetTop+(e.newTrack&&n.offsetTop>=e.top-1?e.h:0),n.offsetHeight,!0)}for(const e of u.values())e.remove();const h=new Map;if(r)for(const e of r.clips){const t=getClip(e.id);t&&!i.has(e.id)&&e.start!==t.start&&h.set(e.id,e.start-t.start)}runtime.dropClipPreview={ids:i,shifts:h,doc:r},applyDropClipPreview()}function dropPlacementLabel(e,t){return e.newTrack?uiText("+ New track"):uiText("Move to {track} · {seconds}s",{track:trackLabel(e.trackId),seconds:t.toFixed(2)})}function showTimelineHint(e,t,n=!1){const i=$("timelineGestureHint");i.textContent=e,i.hidden=!1,i.classList.toggle("limited",n),i.style.left=clamp(t.clientX+14,8,Math.max(8,window.innerWidth-i.offsetWidth-8))+"px",i.style.top=clamp(t.clientY-i.offsetHeight-16,8,Math.max(8,window.innerHeight-i.offsetHeight-8))+"px"}function hideTimelineHint(){$("timelineGestureHint").hidden=!0,delete els.tracksContent.dataset.gesture}function showTrimHint(e,t,n,i,r){const a=e.duration-t.duration;showTimelineHint(`${uiText(a<0?"Trimmed {seconds}s":"Restored {seconds}s",{seconds:Math.abs(a).toFixed(2)})} · ${uiText("Duration {seconds}s",{seconds:e.duration.toFixed(2)})}\n${n.size?uiText("Following clips move together"):uiText("Linked media stays in sync")}`+(r?" · "+uiText("Source or clip limit reached"):""),i,r)}function closeTimelineMenu(){runtime.timelineMenu?.close()}function highlightTimelineGap(e){for(const e of els.tracksContent.querySelectorAll(".timeline-gap-preview"))e.remove();if(e&&!e.reason)for(const t of els.tracks.children){if(!t.dataset.track||!isTrackEnabled(t.dataset.track))continue;const n=document.createElement("div");n.className="timeline-gap-preview",Object.assign(n.style,{left:e.L*state.pps+"px",width:e.G*state.pps+"px",top:t.offsetTop+"px",height:t.offsetHeight+"px"}),els.tracksContent.appendChild(n)}}function openTimelineMenu(e,t){if(closeTimelineMenu(),state.gesture||state.exporting)return;e.preventDefault(),state.playing&&pause(),t&&(state.selIds.has(t.id)?state.selId!==t.id&&setSelection([...state.selIds],t.id):selectClip(t.id));const n=state.time,i=timeAtEvent(e),r=timelineSnapshot(),a=[...state.selIds].join("\0"),o=document.activeElement,s=t?null:sharedGapAt(i),l=document.createElement("div");l.className="ctx-menu timeline-menu",l.setAttribute("role","menu"),l.setAttribute("aria-label",uiText(t?"Clip actions":"Timeline actions"));const c=document.createElement("div");c.className="timeline-menu-heading",c.textContent=t?uiText("Selected {count} clips · linked media follows",{count:state.selIds.size}):uiText("Shared gap on enabled tracks"),l.appendChild(c);const d=[],u=(e,t,n,i,r,a,o)=>d.push({key:e,label:t,shortcut:n,enabled:i,reason:r,run:a,highlight:o});if(t){const e=selectedClips().some(e=>n>e.start+.05&&n<clipEnd(e)-.05),i=uiText("Move the playhead inside the clip first");u("start",uiText("Go to clip start"),"",!0,"",()=>{setTime(t.start),ensurePlayheadVisible()}),u("selection",uiText("Zoom to selected clips"),"Z",!0,"",zoomToSelection),u("split",uiText("Split at playhead"),"S",e,i,splitAtPlayhead),u("trim-in",uiText("Trim start to playhead"),"[",n>t.start&&n<clipEnd(t)-.05,i,()=>trimToPlayhead("in")),u("trim-out",uiText("Trim end to playhead"),"]",n>t.start+.05&&n<clipEnd(t),i,()=>trimToPlayhead("out")),u("delete",uiText("Delete selected clips"),"Del",!0,"",deleteSelected)}else u("gap",uiText("Close shared gap")+(s.reason?"":` · ${s.G.toFixed(2)}s`),"⇧G",!s.reason,s.reason,()=>closeGapAtPlayhead(i),s),u("next-gap",uiText("Find gap"),"G",!0,"",goToNextGap),u("fit",uiText("Fit timeline"),"⇧Z",!0,"",zoomToFit);u("help",uiText("Keyboard shortcuts"),"",!0,"",()=>$("helpOverlay").classList.remove("hidden"));for(const e of d){const t=document.createElement("button");t.type="button",t.className="ctx-opt",t.dataset.action=e.key,t.setAttribute("role","menuitem"),t.disabled=!e.enabled,t.title=e.enabled?e.label:e.reason;const i=document.createElement("span");if(i.textContent=e.label,t.appendChild(i),e.shortcut){const n=document.createElement("kbd");n.textContent=e.shortcut,t.appendChild(n)}if(!e.enabled){const n=document.createElement("small");n.textContent=e.reason,t.appendChild(n)}t.addEventListener("pointerenter",()=>highlightTimelineGap(e.enabled?e.highlight:null)),t.addEventListener("focus",()=>highlightTimelineGap(e.enabled?e.highlight:null)),t.addEventListener("click",()=>{const t=timelineSnapshot()!==r||[...state.selIds].join("\0")!==a;closeTimelineMenu(),t?toast(uiText("Timeline changed. Open the menu again.")):(["split","trim-in","trim-out"].includes(e.key)&&setTime(n),e.run())}),l.appendChild(t)}const p=e=>{l.contains(e.target)||closeTimelineMenu()};runtime.timelineMenu={close:()=>{runtime.timelineMenu=null;const e=l.contains(document.activeElement);l.remove(),highlightTimelineGap(null),document.removeEventListener("pointerdown",p,!0),els.timelineScroll.removeEventListener("scroll",closeTimelineMenu),window.removeEventListener("resize",closeTimelineMenu),e&&o?.isConnected&&o.focus({preventScroll:!0})}},l.addEventListener("keydown",e=>{if(e.stopPropagation(),"Escape"===e.key)return e.preventDefault(),void closeTimelineMenu();const t=[...l.querySelectorAll("button:not(:disabled)")],n=t.indexOf(document.activeElement),i="ArrowDown"===e.key?(n+1)%t.length:"ArrowUp"===e.key?(n-1+t.length)%t.length:"Home"===e.key?0:"End"===e.key?t.length-1:-1;i>=0&&(e.preventDefault(),t[i].focus()),"Tab"===e.key&&closeTimelineMenu()}),l.addEventListener("pointerleave",()=>highlightTimelineGap(null)),document.body.appendChild(l),l.style.left=clamp(e.clientX,8,Math.max(8,window.innerWidth-l.offsetWidth-8))+"px",l.style.top=clamp(e.clientY,8,Math.max(8,window.innerHeight-l.offsetHeight-8))+"px",l.querySelector("button:not(:disabled)")?.focus(),document.addEventListener("pointerdown",p,!0),els.timelineScroll.addEventListener("scroll",closeTimelineMenu),window.addEventListener("resize",closeTimelineMenu)}function movePlacement(e,t,n,i,r,a){const o=structuredClone(e),s=o.clips.find(e=>e.id===t),l=o.clips.filter(e=>n.has(e.id));if(!i.newTrack&&i.trackId===s.track&&Number.isFinite(a)){const e=Math.max(r-s.start,-Math.min(...l.map(e=>e.start))),t=sameTrackMoveDelta(o.clips,l,n,e);if(Math.abs(t-e)>Timeline.EPS&&reorderPlacement(o.clips,s,n,a))return{doc:o,start:s.start};for(const e of l)e.start+=t;return{doc:o,start:s.start}}i.newTrack?s.track=Timeline.insertTrack(o,Timeline.kindOf(s),i.beforeId):s.track=i.trackId;let c=Math.max(r-s.start,-Math.min(...l.map(e=>e.start)));const d=l.filter(e=>e===s||"audio"!==e.kind);let u;do{u=!1;for(const e of d){const t=Timeline.firstGap(o.clips,e.track,e.start+c,e.duration,n);t>e.start+c+Timeline.EPS&&(c=t-e.start,u=!0)}}while(u);for(const e of l)e.start+=c;const p=new Set(n);for(const e of l)p.delete(e.id),"audio"===e.kind&&e!==s&&(e.track=Timeline.freeTrack(o,"audio",e.start,e.duration,e.track,p));return Timeline.normalize(o),{doc:o,start:s.start}}function sameTrackMoveDelta(e,t,n,i){const r=[];for(const i of t)for(const t of e)n.has(t.id)||t.track!==i.track||r.push([t.start-i.start-i.duration,clipEnd(t)-i.start]);r.sort((e,t)=>e[0]-t[0]);const a=[];for(const e of r){const t=a[a.length-1];t&&e[0]<t[1]-Timeline.EPS?t[1]=Math.max(t[1],e[1]):a.push(e)}for(const[e,t]of a)if(i>e+Timeline.EPS&&i<t-Timeline.EPS)return i>0?Math.max(0,e):Math.min(0,t);return i}function reorderPlacement(e,t,n,i){const r=e.filter(e=>e.track===t.track).sort((e,t)=>e.start-t.start),a=r.filter(e=>n.has(e.id)),o=r.indexOf(a[0]),s=t=>{const n=new Set(t.map(e=>e.linkGroup).filter(Boolean)),i=new Set(t.flatMap(e=>[e.id,e.linkedId]).filter(Boolean));return e.filter(e=>n.has(e.linkGroup)||i.has(e.id)||i.has(e.linkedId))};if(r.indexOf(a[a.length-1])-o+1!==a.length)return!1;const l=new Set(s(a).map(e=>e.id));if([...n].some(e=>!l.has(e)))return!1;if(r.some((e,t)=>t&&e.start<clipEnd(r[t-1])-Timeline.EPS))return!1;const c=r.filter(e=>!n.has(e.id)),d=c.filter(e=>i>=e.start+e.duration/2).length;if(d===o)return!1;const u=d<o,p=u?c.slice(d,o):c.slice(o,d),m=a[0].start,f=clipEnd(a[a.length-1]),h=f-m,g=d?clipEnd(c[d-1]):c[0].start,v=d<c.length?c[d].start:g,x=Math.abs(i-g)<=Math.abs(i-v)?g:v,b=new Map([...n].map(e=>[e,x-(u?m:f)]));for(const e of s(p)){const t=u?h:-h;if(b.has(e.id)&&Math.abs(b.get(e.id)-t)>Timeline.EPS)return!1;b.set(e.id,t)}const y=new Map(e.map(e=>[e.id,e.start+(b.get(e.id)||0)]));for(const t of e){if(!b.has(t.id))continue;const n=y.get(t.id);if(n<-Timeline.EPS)return!1;for(const i of e)if(i.id!==t.id&&i.track===t.track&&n<y.get(i.id)+i.duration-Timeline.EPS&&n+t.duration>y.get(i.id)+Timeline.EPS)return!1}for(const t of e)b.has(t.id)&&(t.start=Math.max(0,y.get(t.id)));return!0}function trimGestureBounds(e,t){const n=new Set(t.map(e=>e.id));let i=-1/0,r=1/0;for(const e of t){const t=clipSpeed(e),a=getMedia(e.mediaId),o="video"===e.kind||"audio"===e.kind;i=Math.max(i,-e.start,o?-(e.in||0)/t:-1/0),o&&a?.duration&&(r=Math.min(r,(a.duration-(e.in||0))/t-e.duration));for(const t of project.clips)n.has(t.id)||t.track!==e.track||(clipEnd(t)<=e.start+Timeline.EPS&&(i=Math.max(i,clipEnd(t)-e.start)),t.start>=clipEnd(e)-Timeline.EPS&&(r=Math.min(r,t.start-clipEnd(e))))}return{left:i,right:r}}function updateClipDragScroll(e){const t=runtime.clipDrag;if(!t||!state.gesture||document.hidden)return;const n=els.timelineScroll,i=n.getBoundingClientRect(),r=n.clientWidth,a=t.pointer;if(!r||a.clientY<i.top+n.clientTop||a.clientY>=i.top+n.clientTop+n.clientHeight)return;const o=clamp(a.clientX-i.left-n.clientLeft,0,r),s=Math.min(48,r/4),l=o<s?(o-s)/s:o>r-s?(o-r+s)/s:0;if(!l)return;const c=Math.max(0,Math.min(n.scrollWidth,state.clipGestureWidth)-r),d=clamp(n.scrollLeft+720*l*e,0,c);Math.abs(d-n.scrollLeft)<.01||(runtime.followPlayhead=!1,scrollTimeline(d),t.update(a))}function startClipGesture(e,t,n,i){e.preventDefault(),closeTimelineMenu(),state.gesture=!0,els.tracksContent.dataset.gesture=n,"move"===n&&($("timelineGestureHint").hidden=!0),state.clipGestureWidth=els.tracksContent?.offsetWidth||0;const r={start:t.start,in:t.in,duration:t.duration,track:t.track,keyframes:t.keyframes?JSON.parse(JSON.stringify(t.keyframes)):void 0},a=withLinked("move"===n&&state.selIds.has(t.id)?selectedClips():[t]),o=new Map(a.map(e=>[e.id,{start:e.start,in:e.in,duration:e.duration,keyframes:e.keyframes?JSON.parse(JSON.stringify(e.keyframes)):void 0}])),s=new Set(a.map(e=>e.id)),l="move"===n?null:trimFollowers(t,r),c=project.clips.map(e=>({id:e.id,start:e.start,duration:e.duration}));state.clipTrimOrigins="trim-l"===n?new Map(c.map(e=>[e.id,e.start])):null;let d=1e6,u=1e6;if("move"!==n){const e=clipSpeed(t);for(const t of a){if("video"!==t.kind&&"audio"!==t.kind)continue;d=Math.min(d,r.in/e);const n=getMedia(t.mediaId);n?.duration&&(u=Math.min(u,(n.duration-r.in)/e))}const n=new Set(a.map(e=>e.track)),i=r.start+r.duration;for(const e of project.clips)!s.has(e.id)&&n.has(e.track)&&e.start<i-1e-9&&clipEnd(e)>=i&&(u=Math.min(u,r.duration),d=0)}const p=els.timelineScroll.scrollLeft,m=e.clientX,f=e.clientY,h="move"===n?timeAtEvent(e):0;let g=!1,v=!1;const x=els.tracksContent,b=timelineSnapshot(),y=JSON.parse(b);let T=null;const k=i=>{if(null!=e.pointerId&&null!=i.pointerId&&i.pointerId!==e.pointerId)return;const v=(i.clientX-m+els.timelineScroll.scrollLeft-p)/state.pps;if(!g&&("move"===n?Math.hypot(i.clientX-m,i.clientY-f)>6:Math.abs(i.clientX-m)>3||Math.abs(i.clientY-f)>3)&&(g=!0,"move"===n&&state.playing&&pause()),g){if("move"===n){const e=timelineDropTarget(i,Timeline.kindOf(t));if(!e)return runtime.clipDrag=null,T=null,void clearDropPreview();runtime.clipDrag={pointer:{clientX:i.clientX,clientY:i.clientY,pointerId:i.pointerId},update:k};const n=r.start+v,l=n+r.duration,d=snapTime(n,s,c),u=snapTime(l,s,c),p=Math.abs(d-n),m=Math.abs(u-l),f=m>0&&(0===p||m<p)?u-r.duration:d,g=h+v;T={...movePlacement(y,t.id,s,e,f,g),target:e,proposedStart:f,pointerTime:g};const x=Math.max(n,r.start-Math.min(...a.map(e=>o.get(e.id).start)));showDropPreview(e,T.start,r.duration,s,T.doc,t.id,x)}else if("trim-l"===n){let e=snapTime(r.start+v,s,c);const n=clipSpeed(t);e=clamp(e,r.start-d,r.start+Math.max(0,r.duration-.05));const u=e-r.start;t.start=Math.max(0,e),t.in="video"===t.kind||"audio"===t.kind?r.in+u*n:0,t.duration=r.duration-u;for(const e of a)e.keyframes=shiftKF(o.get(e.id).keyframes,u,t.duration);syncLinkedTiming(t);for(const[t,n]of l){const i=getClip(t);i&&(i.start=n-Math.min(0,e))}showTrimHint(t,r,l,i,Math.abs(e-snapTime(r.start+v,s,c))>Timeline.EPS)}else{const e=snapTime(r.start+r.duration+v,s,c);t.duration=clamp(e-r.start,.05,u);for(const e of a)e.keyframes=shiftKF(o.get(e.id).keyframes,0,t.duration);rippleTrim(t,r,l),showTrimHint(t,r,l,i,Math.abs(t.duration-(e-r.start))>Timeline.EPS)}"move"!==n&&(state.dirtyTimeline=!0,renderInspector(!0))}},S=(a,o={})=>{if(!(v||null!=e.pointerId&&null!=o.pointerId&&o.pointerId!==e.pointerId)){if(v=!0,runtime.clipDrag=null,window.removeEventListener("pointermove",k),window.removeEventListener("pointerup",w),window.removeEventListener("pointercancel",E),window.removeEventListener("keydown",C,!0),window.removeEventListener("blur",M),x.removeEventListener("lostpointercapture",M),x.hasPointerCapture(e.pointerId)&&x.releasePointerCapture(e.pointerId),clearDropPreview(),hideTimelineHint(),!a&&g&&"trim-l"===n&&rippleTrim(t,r,l),state.clipTrimOrigins=null,state.gesture=!1,state.clipGestureWidth=null,a)"move"!==n&&restoreTimeline(b);else if(g&&("move"!==n||T)){let e=b;if(T&&(e=timelineSnapshot(),getClip(t.id))){const n=new Set(withLinked(project.clips.filter(e=>s.has(e.id))).map(e=>e.id)),i=movePlacement(JSON.parse(e),t.id,n,T.target,T.proposedStart,T.pointerTime);restoreTimeline(JSON.stringify(i.doc))}timelineSnapshot()!==e&&(runtime.undo.push(e),runtime.undo.length>100&&runtime.undo.shift(),runtime.redo.length=0,scheduleSave())}else!g&&i&&selectClip(t.id);state.dirtyTimeline=!0,renderInspector(),window.dispatchEvent(new Event("timelinegestureend")),runtime.pendingSync&&syncFromServer()}},w=e=>S(!1,e),E=e=>S(!0,e),M=e=>S(!1,e),C=e=>{"Escape"===e.key&&(e.preventDefault(),e.stopImmediatePropagation(),S(!0))};window.addEventListener("pointermove",k),window.addEventListener("pointerup",w),window.addEventListener("pointercancel",E),window.addEventListener("keydown",C,!0),window.addEventListener("blur",M),x.addEventListener("lostpointercapture",M);try{x.setPointerCapture(e.pointerId)}catch{}}function startMarquee(e){e.preventDefault(),state.gesture=!0;const t=e.ctrlKey||e.metaKey||e.shiftKey?new Set(state.selIds):new Set,n=els.tracksContent.getBoundingClientRect(),i=e.clientX-n.left,r=e.clientY-n.top,a=new Map;for(const e of els.tracks.children)a.set(e.dataset.track,{top:e.offsetTop,h:e.offsetHeight});let o=null,s=!1;const l=e=>{const n=els.tracksContent.getBoundingClientRect(),l=e.clientX-n.left,c=e.clientY-n.top;if(!s&&Math.hypot(l-i,c-r)<4)return;s=!0,o||(o=document.createElement("div"),o.className="marquee",els.tracksContent.appendChild(o));const d=Math.min(i,l),u=Math.min(r,c),p=Math.abs(l-i),m=Math.abs(c-r);Object.assign(o.style,{left:d+"px",top:u+"px",width:p+"px",height:m+"px"});const f=new Set(t);for(const e of project.clips){const t=a.get(e.track);if(!t)continue;const n=e.start*state.pps,i=n+Math.max(8,e.duration*state.pps);n<d+p&&i>d&&t.top<u+m&&t.top+t.h>u&&f.add(e.id)}state.selIds=f;for(const e of els.tracks.querySelectorAll(".clip"))e.classList.toggle("selected",f.has(e.dataset.id))},c=e=>{window.removeEventListener("pointermove",l),window.removeEventListener("pointerup",c),state.gesture=!1,window.dispatchEvent(new Event("timelinegestureend")),o&&o.remove(),s?(state.selIds.has(state.selId)||(state.selId=[...state.selIds].pop()??null),state.dirtyTimeline=!0,renderInspector(),syncBinSelectionFromTimeline()):(selectClip(null),setTime(timeAtEvent(e))),runtime.pendingSync&&syncFromServer()};window.addEventListener("pointermove",l),window.addEventListener("pointerup",c)}function updateScrub(e=0){if(!runtime.scrub)return;const t=els.timelineScroll,n=t.clientWidth;if(!n)return;const i=clamp(runtime.scrub.clientX-t.getBoundingClientRect().left-t.clientLeft,0,n-1),r=Math.min(48,n/4),a=i<r?(i-r)/r:i>n-r?(i-n+r)/r:0,o=Math.min(t.scrollWidth-n,Math.max(0,Math.ceil(projDur()*state.pps-i))),s=720*a*e;s<0?t.scrollLeft=Math.max(0,t.scrollLeft+s):s>0&&t.scrollLeft<o&&(t.scrollLeft=Math.min(o,t.scrollLeft+s));const l=clamp((t.scrollLeft+i)/state.pps,0,projDur());l!==state.time&&setTime(l)}function startScrub(e){if(0!==e.button||runtime.scrub)return;e.preventDefault(),e.stopPropagation(),state.playing&&pause(),runtime.followPlayhead=!0,state.gesture=!0,runtime.scrub={clientX:e.clientX},silencePreviewMedia();const t=t=>{t.pointerId===e.pointerId&&(runtime.scrub.clientX=t.clientX,updateScrub())};updateScrub();const n=i=>{null!=i.pointerId&&i.pointerId!==e.pointerId||(window.removeEventListener("pointermove",t),window.removeEventListener("pointerup",n),window.removeEventListener("pointercancel",n),window.removeEventListener("blur",n),els.ruler.removeEventListener("lostpointercapture",n),els.ruler.hasPointerCapture(e.pointerId)&&els.ruler.releasePointerCapture(e.pointerId),runtime.scrub=null,state.gesture=!1,window.dispatchEvent(new Event("timelinegestureend")),runtime.pendingSync&&syncFromServer())};window.addEventListener("pointermove",t),window.addEventListener("pointerup",n),window.addEventListener("pointercancel",n),window.addEventListener("blur",n),els.ruler.addEventListener("lostpointercapture",n);try{els.ruler.setPointerCapture(e.pointerId)}catch{}}function setTime(e){resetPreviewWait(!0),state.time=clamp(e,0,Math.max(projDur(),0)),seekMediaWhilePaused(),state.audioHold&&scheduleAudioHoldRefresh()}function keyframeTimelineTimes(e){const t=new Set,n=[];for(const i of e)for(const e of clipKeyframeLocalTimes(i)){const r=+(i.start+e).toFixed(4);t.has(r)||(t.add(r),n.push(r))}return n.sort((e,t)=>e-t),n}function goToKeyframe(e){let t=state.selIds.size?selectedClips():[];if(!t.some(e=>clipKeyframeLocalTimes(e).length)){const e=state.time;t=project.clips.filter(t=>clipKeyframeLocalTimes(t).length&&e>=t.start-1e-6&&e<=t.start+t.duration+1e-6)}const n=keyframeTimelineTimes(t);if(!n.length)return void toast(uiText("No keyframes"));const i=.5/Math.max(1,project.fps||30);if(e>0){const e=n.find(e=>e>state.time+i);if(null==e)return void toast(uiText("No next keyframe"));setTime(e)}else{let e=null;for(const t of n)t<state.time-i&&(e=t);if(null==e)return void toast(uiText("No previous keyframe"));setTime(e)}}function toggleMarker(){const e=+state.time.toFixed(3),t=Math.max(.05,8/state.pps);project.markers=project.markers||[];const n=project.markers.findIndex(n=>Math.abs(n.t-e)<t);n>=0&&!state.playing?project.markers.splice(n,1):(project.markers.push({t:e}),project.markers.sort((e,t)=>e.t-t.t)),scheduleSave()}function workAreaTime(){return Math.max(0,+state.time.toFixed(3))}function setInPoint(){const e=workAreaTime(),t=project.outPoint,{inPoint:n,outPoint:i}=normalizeWorkArea(e,t);null!=t&&null==n&&null==i&&toast(Math.abs(e-t)<1e-6?uiText("IN and OUT must be at different times"):uiText("IN must be before OUT")),project.inPoint=n,project.outPoint=i,updateWorkArea(),syncTrimIOButton(),scheduleSave()}function setOutPoint(){const e=workAreaTime(),t=project.inPoint,{inPoint:n,outPoint:i}=normalizeWorkArea(t,e);null!=t&&null==n&&null==i&&toast(Math.abs(e-t)<1e-6?uiText("IN and OUT must be at different times"):uiText("OUT must be after IN")),project.inPoint=n,project.outPoint=i,updateWorkArea(),syncTrimIOButton(),scheduleSave()}function clearInPoint(){null!=project.inPoint&&(project.inPoint=null,updateWorkArea(),syncTrimIOButton(),scheduleSave())}function clearOutPoint(){null!=project.outPoint&&(project.outPoint=null,updateWorkArea(),syncTrimIOButton(),scheduleSave())}function updateWorkArea(){const e=$("workDimL"),t=$("workDimR");if(!e||!t)return;const n=project.inPoint,i=project.outPoint,r=els.tracksContent.offsetWidth||contentWidth();if(null==n||null==i||i<=n)return e.classList.add("hidden"),void t.classList.add("hidden");const a=clamp(n*state.pps,0,r),o=clamp(i*state.pps,0,r);e.classList.remove("hidden"),t.classList.remove("hidden"),e.style.width=Math.max(0,a)+"px",t.style.left=o+"px",t.style.width=Math.max(0,r-o)+"px"}els.tracksContent.addEventListener("contextmenu",e=>{const t=timeAtEvent(e),n=trackAtEvent(e);openTimelineMenu(e,project.clips.find(e=>e.track===n&&t>=e.start&&t<clipEnd(e)))}),els.ruler.addEventListener("pointerdown",startScrub),els.playhead.addEventListener("pointerdown",startScrub),els.timelineScroll.addEventListener("dragover",e=>{if(!e.dataTransfer.types.includes("text/fablecut-media"))return;e.preventDefault();const t=getMedia(runtime.dragMediaId);if(!t)return;const n=timelineDropTarget(e,Timeline.kindOf(t));if(e.dataTransfer.dropEffect=n?"copy":"none",!n)return void clearDropPreview();const i=t.duration||5,r=snapTime(timeAtEvent(e),null);showDropPreview(n,n.newTrack?r:Timeline.firstGap(project.clips,n.trackId,r,i),i)}),els.timelineScroll.addEventListener("dragleave",e=>{els.timelineScroll.contains(e.relatedTarget)||clearDropPreview()}),window.addEventListener("dragend",()=>{runtime.dragMediaId=null,clearDropPreview()}),els.timelineScroll.addEventListener("drop",e=>{const t=e.dataTransfer.getData("text/fablecut-media");if(runtime.dragMediaId=null,!t)return void clearDropPreview();e.preventDefault();const n=getMedia(t),i=n?timelineDropTarget(e,Timeline.kindOf(n)):null;if(clearDropPreview(),!i)return;const r=snapTime(timeAtEvent(e),null);null==n.duration&&"image"!==n.kind&&"svg"!==n.kind||"svg"===n.kind&&!runtime.mediaAux.get(n.id)?.svgText?addLibraryItem({name:n.name,src:n.src},i.trackId,r,i):addClipFromMedia(n,i.trackId,r,i)});let dragDepth=0;function timelineMinZoom(){const e=projDur();return e>0?Math.min(300,.95*(els.timelineScroll.clientWidth||800)/e):1}function updateTimelineZoomControls(){const e=timelineMinZoom();state.gesture||(state.pps=clamp(state.pps,e,300)),els.zoomSlider.min=e,els.zoomSlider.step="any",els.zoomSlider.value=state.pps,$("btnZoomOut").disabled=state.pps<=e,$("btnZoomIn").disabled=state.pps>=300}function setZoom(e,t){const n=els.timelineScroll,i=n.getBoundingClientRect(),r=null!=t?t-i.left:i.width/2,a=(n.scrollLeft+r)/state.pps;state.pps=clamp(e,timelineMinZoom(),300),els.zoomSlider.value=state.pps,state.dirtyTimeline=!0,rebuildClips(),scrollTimeline(Math.max(0,a*state.pps-r))}function zoomToFit(){setZoom(.95*(els.timelineScroll.clientWidth||800)/(projDur()||1)),scrollTimeline(0)}function zoomToSelection(){const e=selectedClips();e.length?zoomToRange(Math.min(...e.map(e=>e.start)),Math.max(...e.map(e=>e.start+e.duration))):toast(uiText("Select a clip to zoom to"))}function zoomToWorkArea(){const e=project.inPoint,t=project.outPoint;null==e||null==t||t<=e?toast(uiText("Set IN and OUT markers first (I / O)")):zoomToRange(e,t)}function zoomToRange(e,t){const n=Math.max(t-e,.05),i=els.timelineScroll.clientWidth||800,r=clamp(.9*i/n,timelineMinZoom(),300);state.pps=r,els.zoomSlider.value=r,rebuildClips();const a=(e+t)/2,o=Math.max(0,contentWidth()-i);scrollTimeline(clamp(a*r-i/2,0,o))}function selectedMediaIds(){const e=new Set;for(const t of selectedClips())t.mediaId&&e.add(t.mediaId);return e}function clearBinSelectionHighlight(){if(els.binList)for(const e of els.binList.querySelectorAll(".bin-item.selected"))e.classList.remove("selected")}function syncBinSelectionFromTimeline(){if(!els.binList||!getSetting("linkSelect"))return;const e=selectedMediaIds();for(const t of els.binList.querySelectorAll(".bin-item[data-media-id]"))t.classList.toggle("selected",e.has(t.dataset.mediaId))}function selectClipsByMediaId(e){const t=project.clips.filter(t=>t.mediaId===e);if(!t.length)return setSelection([]),void toast(uiText("No clips on the timeline use this media"));t.sort((e,t)=>e.start-t.start||String(e.id).localeCompare(String(t.id)));const n=t.find(e=>"video"===e.kind)||t[0];"project"!==state.binTab&&setBinTab("project"),setSelection(t.map(e=>e.id),n.id)}function setSelection(e,t){runtime.timelineMenu&&closeTimelineMenu(),state.selIds=new Set(e),state.selId=void 0!==t?t:[...state.selIds].pop()??null,state.selId&&!state.selIds.has(state.selId)&&state.selIds.add(state.selId),state.dirtyTimeline=!0,renderInspector(),syncBinSelectionFromTimeline()}function selectClip(e,t){if(t&&t.toggle&&null!=e){const t=new Set(state.selIds);return state.transFocus=null,void(t.has(e)?(t.delete(e),setSelection([...t])):(t.add(e),setSelection([...t],e)))}const n=null==e?null:t?.transFocus??null;state.selId===e&&state.selIds.size<=1&&n===state.transFocus||(state.transFocus=n,setSelection(null==e?[]:[e],e??null))}function pruneSelection(){state.selIds=new Set([...state.selIds].filter(e=>getClip(e))),state.selIds.has(state.selId)||(state.selId=[...state.selIds].pop()??null),syncBinSelectionFromTimeline()}window.addEventListener("dragenter",e=>{e.dataTransfer?.types.includes("Files")&&(dragDepth++,document.body.classList.add("file-drag"))}),window.addEventListener("dragleave",()=>{--dragDepth<=0&&(dragDepth=0,document.body.classList.remove("file-drag"))}),window.addEventListener("dragover",e=>{e.dataTransfer?.types.includes("Files")&&e.preventDefault()}),window.addEventListener("drop",e=>{dragDepth=0,document.body.classList.remove("file-drag"),e.dataTransfer?.files.length&&(e.preventDefault(),importFiles(e.dataTransfer.files))}),els.zoomSlider.addEventListener("input",()=>setZoom(+els.zoomSlider.value)),$("btnZoomOut").addEventListener("click",()=>setZoom(state.pps/1.25)),$("btnZoomIn").addEventListener("click",()=>setZoom(1.25*state.pps)),$("btnZoomFit").addEventListener("click",zoomToFit),els.timelineScroll.addEventListener("wheel",e=>{e.preventDefault(),(e.ctrlKey||e.metaKey)&&(e.preventDefault(),setZoom(state.pps*(e.deltaY<0?1.15:1/1.15),e.clientX))},{passive:!1});const selectedClips=()=>project.clips.filter(e=>state.selIds.has(e.id));function focusTextContent(){if("text"!==getClip(state.selId)?.kind)return;const e=els.inspector.querySelector('textarea[data-k="text"]');e&&(e.focus({preventScroll:!0}),e.select(),e.scrollIntoView({block:"nearest"}))}function renderInspector(e){e&&getClip(state.selId)||closeFontPicker();const t=getClip(state.selId);if(!t)return els.inspector.innerHTML=`<div class="inspector-empty">${uiText("Select a clip to edit its")}<br>${uiText("transform, effects & audio.")}</div>`,void renderKfGraphsPanel();if(e){const e=els.inspector.querySelector("[data-k=start]"),n=els.inspector.querySelector("[data-k=duration]");return e&&(e.value=t.start.toFixed(2)),void(n&&(n.value=t.duration.toFixed(2)))}const n=t.props,i=e=>t.keyframes&&t.keyframes[e]?t.keyframes[e].length:0,r=(e,t,n="",r)=>`<div class="insp-row">${((e,t="",n)=>{e=escapeHtml(uiText(e));const r=void 0!==n?n:t,a=(Array.isArray(r)?r:String(r||"").split(",")).map(e=>e.trim()).filter(Boolean),o=a.some(e=>Object.hasOwn(DEFAULT_PROPS,e)||"transIn"===e||"transOut"===e),s=!!t&&ANIMATABLE.includes(t);return s||o?`<label class="${[s?"kf-graph-toggle":"",s&&state.kfGraphs.has(t)?"on":"",s&&i(t)?"has-kf":"",o?"insp-reset":""].filter(Boolean).join(" ")}"${(s?` data-kfgraph="${t}"`:"")+(o?` data-reset="${a.join(",")}"`:"")} title="${uiAttr(s&&o?"Click: keyframe graph · Ctrl-click: reset":s?"Show / hide keyframe graph":"Ctrl-click to reset")}">${e}</label>`:`<label>${e}</label>`})(e,n,r)}${t}${n?(e=>ANIMATABLE.includes(e)?`<span class="kf-ctl"><button class="kf-btn${i(e)?" has":""}" data-kf="${e}" title="${uiAttr("Set keyframe at playhead")}">◆${i(e)||""}</button>${i(e)?`<button class="kf-btn" data-kfclear="${e}" title="${uiAttr("Clear keyframes")}">✕</button>`:""}</span>`:"")(n):""}</div>`,a=(e,t,n,i,a,o="")=>r(e[0].toUpperCase()+e.slice(1),`<input type="range" data-k="${e}" min="${t}" max="${n}" step="${i}" value="${a}">\n <span class="val" data-val="${e}">${a}${o}</span>`,e);let o=(state.selIds.size>1?`<div class="insp-multi">${uiText("{count} clips selected — drag moves them together, Del deletes all. Fields below edit the primary (white-outlined) clip.",{count:state.selIds.size})}</div>`:"")+`<div class="insp-section"><h3>${uiText("Clip — {kind}",{kind:uiText(t.kind)})}</h3>\n ${"text"===t.kind?"":r("Name",`<input type="text" data-k="name" value="${escapeHtml(t.name)}">`)}\n ${t.mediaId?r("Source",`<button type="button" class="btn tiny style-picker-btn" data-media-open title="${uiAttr("Replace this clip's media — keeps position, trim, keyframes and effects")}">${escapeHtml((getMedia(t.mediaId)||{}).name||uiText("Missing media"))} ▾</button>`):""}\n ${r("Start (s)",`<input type="number" data-k="start" step="0.01" value="${t.start.toFixed(2)}">`)}\n ${r("Length (s)",`<input type="number" data-k="duration" step="0.01" value="${t.duration.toFixed(2)}">`)}\n </div>`;const s=(e,t,n,i)=>r(e,`<select data-k="${t}">${n.map(e=>`<option value="${e}" ${String(e)===String(i)?"selected":""}>${escapeHtml(uiText(String(e)))}</option>`).join("")}</select>`,t),l=(e,t,n)=>r(e,`<input type="checkbox" data-k="${t}" ${n?"checked":""}>`,t);if("adjust"===t.kind?o+=`<div class="insp-section"><h3>${uiText("Adjustment layer")}</h3>\n ${a("opacity",0,1,.01,n.opacity)}\n </div>`:"audio"!==t.kind&&(o+=`<div class="insp-section"><h3>${uiText("Transform")}</h3>\n ${r("Position X",`<input type="number" data-k="x" value="${n.x}">`,"x")}\n ${r("Position Y",`<input type="number" data-k="y" value="${n.y}">`,"y")}\n ${a("scale",.1,4,.01,n.scale)}\n ${a("rotation",-180,180,1,n.rotation,"°")}\n ${a("opacity",0,1,.01,n.opacity)}\n ${s("Blend","blend",BLEND_MODES,n.blend)}\n </div>`),"video"!==t.kind&&"image"!==t.kind&&"svg"!==t.kind||(o+=`<div class="insp-section"><h3>${uiText("Layout")}</h3>\n ${s("Fit","fit",["contain","cover","stretch","none"],n.fit)}\n ${r("Crop L/R %",`<input type="number" data-k="cropL" min="0" max="95" value="${n.cropL}" style="max-width:58px">\n <input type="number" data-k="cropR" min="0" max="95" value="${n.cropR}" style="max-width:58px">`,"","cropL,cropR")}\n ${r("Crop T/B %",`<input type="number" data-k="cropT" min="0" max="95" value="${n.cropT}" style="max-width:58px">\n <input type="number" data-k="cropB" min="0" max="95" value="${n.cropB}" style="max-width:58px">`,"","cropT,cropB")}\n ${a("cornerRadius",0,300,1,n.cornerRadius,"px")}\n ${l("Flip H","flipH",n.flipH)}\n ${l("Flip V","flipV",n.flipV)}\n </div>`),"video"!==t.kind&&"image"!==t.kind&&"svg"!==t.kind&&"adjust"!==t.kind||(o+=`<div class="insp-section"><h3>${uiText("Filter / Color")}</h3>\n ${s("Preset","filterPreset",Object.keys(FILTER_PRESETS),n.filterPreset)}\n ${a("brightness",0,200,1,n.brightness,"%")}\n ${a("contrast",0,200,1,n.contrast,"%")}\n ${a("saturation",0,200,1,n.saturation,"%")}\n ${a("hue",-180,180,1,n.hue,"°")}\n ${a("temperature",-100,100,1,n.temperature)}\n ${a("tint",-100,100,1,n.tint)}\n ${a("blur",0,20,.5,n.blur,"px")}\n ${a("grayscale",0,100,1,n.grayscale,"%")}\n ${a("sepia",0,100,1,n.sepia,"%")}\n ${a("invert",0,100,1,n.invert,"%")}\n ${a("vignette",0,100,1,n.vignette,"%")}\n </div>\n <div class="insp-section"><h3>${uiText("Motion FX")}</h3>\n ${a("shake",0,40,.5,n.shake,"px")}\n ${a("shakeSpeed",1,30,.5,n.shakeSpeed)}\n ${a("rgbSplit",0,30,.5,n.rgbSplit,"px")}\n ${a("grain",0,100,1,n.grain,"%")}\n </div>`),"video"!==t.kind&&"image"!==t.kind||(o+=`<div class="insp-section"><h3>${uiText("Keying / Cut-out")}</h3>\n ${r("Key color",`<input type="color" data-k="chromaKey" value="${n.chromaKey||"#00ff00"}">\n <button class="btn tiny${n.chromaKey?"":" toggle on"}" data-action="keyoff" title="${uiAttr("Disable chroma key")}">${uiText("off")}</button>`,"","chromaKey")}\n ${a("chromaTolerance",0,100,1,n.chromaTolerance)}\n ${a("chromaSoftness",0,100,1,n.chromaSoftness)}\n ${l("AI bg remove","bgRemove",n.bgRemove)}\n </div>`),"video"===t.kind||"audio"===t.kind){const e=t.props?.audioChannel,i=0===e?uiText("Left"):1===e?uiText("Right"):Number.isInteger(e)?uiText("Channel {number}",{number:e+1}):null;o+=`<div class="insp-section"><h3>${uiText("Audio / Time")}</h3>\n ${i?r("Channel",`<span style="opacity:.75">${i}</span>`):""}\n ${a("volume",0,2,.01,n.volume)}\n ${a("speed",.25,4,.05,n.speed,"×")}\n </div>`}const c=(e,t,n)=>`<div class="insp-row${state.transFocus===("transIn"===t?"in":"out")?" trans-active":""}"><label class="insp-reset" data-reset="${t}" title="${uiAttr("Ctrl-click to reset")}">${escapeHtml(uiText(e))}</label>\n <span class="insp-ctrls"><select data-k="${t}">${TRANSITIONS.map(e=>`<option value="${e}" ${e===(n?.type||"none")?"selected":""}>${uiText(e)}</option>`).join("")}</select>\n <input type="number" class="insp-dur" data-k="${t}Dur" step="0.1" min="0.1" value="${n?.duration??1}"></span></div>`;"text"===t.kind&&(o+=`<div class="insp-section"><h3>${uiText("Text")}</h3>\n ${r("Content",`<textarea data-k="text">${escapeHtml(n.text)}</textarea>`,"","text")}\n ${r(hasTextBox(n)&&n.boxFit?"Max size":"Font size",`<input type="range" data-k="fontSize" min="12" max="300" step="1" value="${n.fontSize}">\n <span class="val" data-val="fontSize">${n.fontSize}px</span>`,"fontSize")}\n ${r("Box W/H",`<span class="insp-ctrls">\n <input type="number" data-k="boxW" min="0" step="1" value="${n.boxW||0}" title="${uiAttr("Width in px (0 = no box — hug content)")}" style="max-width:64px">\n <input type="number" data-k="boxH" min="0" step="1" value="${n.boxH||0}" title="${uiAttr("Height in px (0 = no box — hug content)")}" style="max-width:64px">\n </span>`,"","boxW,boxH")}\n ${hasTextBox(n)?l("Scale to fit","boxFit",!!n.boxFit):""}\n ${r("Color",`<span class="insp-ctrls"><input type="color" data-k="color" value="${n.color}">\n <input type="color" data-k="color2" value="${n.color2||n.color}" title="${uiAttr("Gradient bottom color")}">\n <button class="btn tiny${n.color2?"":" toggle on"}" data-action="grad-off" title="${uiAttr("Disable gradient")}">${uiText("flat")}</button></span>`,"","color,color2")}\n ${s("Align","align",["left","center","right","justify"],n.align)}\n ${hasTextBox(n)?s("V-align","vAlign",["top","middle","bottom"],n.vAlign||"middle"):""}\n ${s("Direction","direction",["auto","ltr","rtl"],n.direction||"auto")}\n </div>\n <div class="insp-section"><h3>${uiText("Font")}</h3>\n ${r("Family",`<button type="button" class="btn tiny style-picker-btn" data-font-open aria-haspopup="listbox" aria-expanded="false" aria-label="${uiAttr("Family")}">${escapeHtml(n.font)} ▾</button>`,"","font")}\n ${s("Weight","weight",[0,300,400,500,600,700,800,900],n.weight)}\n ${l("Bold","bold",n.bold)}\n ${l("Italic","italic",n.italic)}\n ${l("Uppercase","uppercase",n.uppercase)}\n ${a("letterSpacing",-10,60,.5,n.letterSpacing,"px")}\n ${a("lineHeight",.7,2.5,.05,n.lineHeight)}\n </div>\n <div class="insp-section"><h3>${uiText("Text style")}</h3>\n ${a("strokeWidth",0,20,.5,n.strokeWidth,"px")}\n ${r("Stroke col.",`<input type="color" data-k="strokeColor" value="${n.strokeColor}">`,"","strokeColor")}\n ${r("Bg color",`<input type="color" data-k="bgColor" value="${n.bgColor}">`,"","bgColor")}\n ${a("bgOpacity",0,1,.05,n.bgOpacity)}\n ${a("textShadow",0,40,1,n.textShadow)}\n ${a("glow",0,100,1,n.glow)}\n ${r("Glow color",`<input type="color" data-k="glowColor" value="${n.glowColor||n.color}">\n <button class="btn tiny${n.glowColor?"":" toggle on"}" data-action="glow-auto" title="${uiAttr("Glow uses the text color")}">${uiText("auto")}</button>`,"","glowColor")}\n </div>\n <div class="insp-section"><h3>${uiText("Text")}</h3>\n ${r("Text style",`<button type="button" class="btn tiny style-picker-btn" data-style-open title="${uiAttr("Pick a style — hover to preview it live")}">${uiText((TITLE_STYLES[t.styleName]||{}).label||"Choose…")} ▾</button>\n <button class="btn tiny" data-action="title-shuffle" title="${uiAttr("Random style")}">${uiText("Shuffle")}</button>`)}\n ${r("Animation",`<select data-k="textAnim">${TEXT_ANIMS.map(e=>`<option value="${e}" ${e===n.textAnim?"selected":""}>${uiText(e)}</option>`).join("")}</select>`,"","textAnim")}\n ${a("wordRate",.05,.6,.01,n.wordRate,"s")}\n </div>`),o+=`<div class="insp-section"><h3>${uiText("Transition")}</h3>\n ${c("In","transIn",t.transitionIn)}\n ${c("Out","transOut",t.transitionOut)}\n </div>`,els.inspector.innerHTML=o,els.inspector.querySelectorAll("label.insp-reset[data-reset]").forEach(e=>{e.addEventListener("click",n=>{if(!n.ctrlKey&&!n.metaKey)return;n.preventDefault();const i=e.dataset.reset.split(",").map(e=>e.trim()).filter(Boolean);if(i.length){pushUndo();for(const e of i)"transIn"!==e&&"transOut"!==e?Object.hasOwn(DEFAULT_PROPS,e)&&(t.props[e]=DEFAULT_PROPS[e],t.keyframes?.[e]&&(delete t.keyframes[e],Object.keys(t.keyframes).length||(t.keyframes=void 0),state.dirtyTimeline=!0),"text"===e&&"text"===t.kind&&(t.name=t.props.text),"text"!==e&&"font"!==e||(state.dirtyTimeline=!0),"speed"===e&&syncLinkedSpeed(t)):(t["transIn"===e?"transitionIn":"transitionOut"]=void 0,state.dirtyTimeline=!0);scheduleSave(),renderInspector()}})}),els.inspector.querySelectorAll("[data-k]").forEach(e=>{const n=e.dataset.k;e.addEventListener("input",()=>{let i="checkbox"===e.type?e.checked:"range"===e.type||"number"===e.type?parseFloat(e.value):e.value;if("weight"===n&&(i=+i||0),"name"===n)t.name=String(i),state.dirtyTimeline=!0;else if("start"===n)t.start=Math.max(0,+i||0),syncLinkedTiming(t),state.dirtyTimeline=!0;else if("duration"===n){const e=withLinked([t]),n=trimGestureBounds(t,e);t.duration=clamp(+i||.05,.05,t.duration+n.right),syncLinkedTiming(t),state.dirtyTimeline=!0}else if("transIn"===n||"transOut"===n){const e="transIn"===n?"transitionIn":"transitionOut",r="transIn"===n?"in":"out",a=Math.max(MIN_TRANS_DUR,parseFloat(els.inspector.querySelector(`[data-k="${n}Dur"]`)?.value)||1);t[e]="none"===i?void 0:{type:String(i),duration:a},t[e]&&saveLastTransition(r,t[e]),state.dirtyTimeline=!0}else if("transInDur"===n||"transOutDur"===n){const e="transInDur"===n?"transitionIn":"transitionOut",r="transInDur"===n?"in":"out";t[e]&&(t[e].duration=Math.max(MIN_TRANS_DUR,+i||1),saveLastTransition(r,t[e]),state.dirtyTimeline=!0)}else t.props[n]=i,"text"===n&&(t.name=String(i),state.dirtyTimeline=!0);"speed"===n&&syncLinkedSpeed(t),"start"!==n&&"duration"!==n||(e.value=t[n].toFixed(2));const r=els.inspector.querySelector(`[data-val="${n}"]`);r&&(r.textContent=e.value),scheduleSave()}),e.addEventListener("focus",()=>pushUndo(),{once:!0})}),els.inspector.querySelectorAll("[data-action]").forEach(e=>{e.addEventListener("click",()=>{const n=e.dataset.action;if(pushUndo(),"keyoff"===n)t.props.chromaKey="";else if("grad-off"===n)t.props.color2="";else if("glow-auto"===n)t.props.glowColor="";else if("title-shuffle"===n){const e=Object.keys(TITLE_STYLES).filter(e=>"plain"!==e&&e!==t.styleName);applyTitleStyle(t,e[Math.floor(Math.random()*e.length)],{keepTransform:!0})}scheduleSave(),renderInspector()})});const d=els.inspector.querySelector("[data-font-open]");if(d&&(d.style.fontFamily=`${JSON.stringify(n.font)}, sans-serif`,d.addEventListener("click",()=>{runtime.fontMenu?closeFontPicker():openFontPicker(d,t)}),d.addEventListener("keydown",e=>{e.stopPropagation(),"ArrowDown"!==e.key&&"ArrowUp"!==e.key||(e.preventDefault(),runtime.fontMenu||openFontPicker(d,t))})),els.inspector.querySelectorAll("[data-style-open]").forEach(e=>{e.addEventListener("click",n=>{n.stopPropagation(),runtime.styleMenu?closeStylePicker():openStylePicker(e,t)})}),els.inspector.querySelectorAll("[data-media-open]").forEach(e=>{e.addEventListener("click",n=>{n.stopPropagation(),runtime.mediaMenu?closeMediaPicker():openMediaPicker(e,t)})}),els.inspector.querySelectorAll("[data-kf]").forEach(e=>{e.addEventListener("click",()=>{const n=e.dataset.kf,i=els.inspector.querySelector(`[data-k="${n}"]`),r=i?parseFloat(i.value):+(t.props[n]||0);if(isNaN(r))return;pushUndo(),t.keyframes||(t.keyframes={});const a=t.keyframes[n]=t.keyframes[n]||[],o=+clamp(state.time-t.start,0,t.duration).toFixed(3),s=a.find(e=>Math.abs(e.t-o)<.5/project.fps);s?s.v=r:a.push({t:o,v:r}),a.sort((e,t)=>e.t-t.t),"speed"===n&&syncLinkedSpeed(t),state.dirtyTimeline=!0,scheduleSave(),renderInspector()})}),els.inspector.querySelectorAll("[data-kfclear]").forEach(e=>{e.addEventListener("click",()=>{pushUndo(),delete t.keyframes[e.dataset.kfclear],Object.keys(t.keyframes).length||(t.keyframes=void 0),"speed"===e.dataset.kfclear&&syncLinkedSpeed(t),state.dirtyTimeline=!0,scheduleSave(),renderInspector()})}),state.transFocus){const e="in"===state.transFocus?"transIn":"transOut",t=els.inspector.querySelector(`[data-k="${e}"]`)?.closest(".insp-row");t?.scrollIntoView({block:"nearest",behavior:"smooth"})}els.inspector.querySelectorAll("[data-kfgraph]").forEach(e=>{e.addEventListener("click",t=>{t.ctrlKey||t.metaKey||(t.preventDefault(),toggleKfGraph(e.dataset.kfgraph))})}),renderKfGraphsPanel()}const KF_GRAPH_LABEL={x:"Pos X",y:"Pos Y",scale:"Scale",rotation:"Rotation",opacity:"Opacity",volume:"Volume",speed:"Speed",brightness:"Bright",contrast:"Contrast",saturation:"Sat",hue:"Hue",blur:"Blur",grayscale:"Gray",sepia:"Sepia",invert:"Invert",temperature:"Temp",tint:"Tint",vignette:"Vignette",cornerRadius:"Radius",shake:"Shake",rgbSplit:"RGB",grain:"Grain",fontSize:"Size",letterSpacing:"Track",glow:"Glow"};function toggleKfGraph(e){ANIMATABLE.includes(e)&&(state.kfGraphs.has(e)?state.kfGraphs.delete(e):state.kfGraphs.add(e),renderInspector())}function renderKfGraphsPanel(){const e=els.kfGraphs;if(!e)return;const t=getClip(state.selId),n=[...state.kfGraphs].filter(e=>ANIMATABLE.includes(e));if(!t||!n.length)return e.innerHTML="",void(e.hidden=!0);e.hidden=!1,e.innerHTML=n.map(e=>`<div class="kf-graph" data-kfgraph-card="${e}">\n <div class="kf-graph-head"><span>${uiText(KF_GRAPH_LABEL[e]||e)}</span>\n <button type="button" data-kfgraph-close="${e}" title="${uiAttr("Close graph")}">✕</button></div>\n <canvas width="160" height="52"></canvas>\n </div>`).join(""),e.querySelectorAll("[data-kfgraph-close]").forEach(e=>{e.addEventListener("click",t=>{t.stopPropagation(),state.kfGraphs.delete(e.dataset.kfgraphClose),renderInspector()})}),e.querySelectorAll(".kf-graph canvas").forEach(e=>{const t=e.closest("[data-kfgraph-card]")?.dataset.kfgraphCard;e.addEventListener("pointerdown",n=>seekKfGraph(n,e,t))}),updateKfGraphs()}function seekKfGraph(e,t,n){const i=getClip(state.selId);if(!i||!n)return;const{t0:r,t1:a}=kfGraphRange(i,n),o=t.getBoundingClientRect(),s=clamp((e.clientX-o.left)/Math.max(1,o.width),0,1);setTime(i.start+r+s*Math.max(1e-6,a-r)),updateKfGraphs()}function kfGraphRange(e,t){const n=+(e.props?.[t]??DEFAULT_PROPS[t]??0),i=Math.max(.05,e.duration),r=Array.isArray(e.keyframes?.[t])?e.keyframes[t]:[],a=r.length?r.map(e=>+e.v):[n];let o=Math.min(...a),s=Math.max(...a);if(isFinite(o)&&isFinite(s)||(o=0,s=1),Math.abs(s-o)<1e-6){const e=Math.max(.05,.1*Math.abs(o)||.5);o-=e,s+=e}else{const e=.12*(s-o);o-=e,s+=e}let l=0,c=i;if(1===r.length){const e=clamp(+r[0].t||0,0,i),t=Math.max(.15,.08*i);l=Math.max(0,e-t),c=Math.min(i,e+t)}else if(r.length>=2){const e=r.map(e=>+e.t||0),t=Math.min(...e),n=Math.max(...e),a=Math.max(.05,.1*(n-t),.02*i);l=Math.max(0,t-a),c=Math.min(i,n+a)}return c-l<.001&&(l=0,c=i),{lo:o,hi:s,fallback:n,t0:l,t1:c,dur:i}}function updateKfGraphs(){const e=els.kfGraphs;if(!e||e.hidden)return;const t=getClip(state.selId);t&&e.querySelectorAll(".kf-graph").forEach(e=>{const n=e.dataset.kfgraphCard,i=e.querySelector("canvas");n&&i&&drawKfGraph(i,t,n)})}function drawKfGraph(e,t,n){const i=window.devicePixelRatio||1,r=e.clientWidth||160,a=e.clientHeight||52;e.width===Math.round(r*i)&&e.height===Math.round(a*i)||(e.width=Math.round(r*i),e.height=Math.round(a*i));const o=e.getContext("2d");o.setTransform(i,0,0,i,0,0);const s=r,l=a;o.clearRect(0,0,s,l);const{lo:c,hi:d,fallback:u,t0:p,t1:m}=kfGraphRange(t,n),f=Math.max(1e-6,m-p),h=e=>l-4-(e-c)/(d-c)*(l-8),g=e=>3+(e-p)/f*(s-6);o.strokeStyle="#ffffff10",o.lineWidth=1,o.beginPath();const v=h(0);v>4&&v<l-4&&(o.moveTo(3,v),o.lineTo(s-3,v),o.stroke()),o.strokeStyle="#7b6cff",o.lineWidth=1.5,o.beginPath();const x=Math.max(24,Math.floor(s));for(let e=0;e<=x;e++){const i=p+e/x*f,r=kfChannel(t,n,i,u),a=g(i),s=h(r);0===e?o.moveTo(a,s):o.lineTo(a,s)}o.stroke();const b=t.keyframes?.[n];if(Array.isArray(b)){o.fillStyle="#ffd166";for(const e of b){const t=g(e.t),n=h(e.v);o.beginPath(),o.moveTo(t,n-3.5),o.lineTo(t+3.5,n),o.lineTo(t,n+3.5),o.lineTo(t-3.5,n),o.closePath(),o.fill()}}const y=state.time-t.start;if(y>=p-1e-6&&y<=m+1e-6){const e=g(clamp(y,p,m));o.strokeStyle="#ff4d6a",o.lineWidth=1,o.beginPath(),o.moveTo(e,1),o.lineTo(e,l-1),o.stroke();const i=kfChannel(t,n,clamp(y,p,m),u);o.fillStyle="#ff4d6a",o.beginPath(),o.arc(e,h(i),2.5,0,2*Math.PI),o.fill()}}function getClipEl(e){let t=runtime.clipEls.get(e.id);if(t)return t;const n=getMedia(e.mediaId);if(!n)return null;t=document.createElement("audio"===e.kind?"audio":"video"),t.preload="auto",t.src=mediaLoadSrc(n),t.playsInline=!0;const i=n.src;return t.addEventListener("error",()=>{runtime.clipEls.get(e.id)===t&&n.src===i&&setMediaFailure(i,"unreadable")}),runtime.clipEls.set(e.id,t),hookAudio(e,t),t}function releaseClipEl(e){runtime.previewMedia.delete(e);const t=runtime.clipEls.get(e);if(t){try{t.pause(),t.removeAttribute("src"),t.load()}catch{}runtime.clipEls.delete(e)}const n=runtime.clipGain.get(e);if(n){try{n.disconnect()}catch{}if(n._fcSource)try{n._fcSource.disconnect()}catch{}if(n._fcOut)try{n._fcOut.disconnect()}catch{}if(n._fcSplit)try{n._fcSplit.disconnect()}catch{}runtime.clipGain.delete(e)}}function stealExportVideo(e,t){if(runtime.clipEls.get(e.id))return runtime.clipEls.get(e.id);let n=null,i=null;for(const[r,a]of runtime.clipEls){const o=getClip(r);o&&"video"===o.kind&&o.mediaId===e.mediaId&&!activeAt(o,t)&&(!i||a.readyState>=1&&i.readyState<1)&&(n=r,i=a)}if(!i)return null;runtime.clipEls.delete(n),runtime.previewMedia.delete(n);const r=runtime.clipGain.get(n);return r&&(runtime.clipGain.delete(n),runtime.clipGain.set(e.id,r)),runtime.clipEls.set(e.id,i),i}function acquireExportVideo(e,t){return runtime.clipEls.get(e.id)||stealExportVideo(e,t)||getClipEl(e)}function releaseFinishedExportVideos(e){for(const t of[...runtime.clipEls.keys()]){const n=getClip(t);"video"!==n?.kind||activeAt(n,e)||releaseClipEl(t)}}function ensureAudio(){if(runtime.audio)return runtime.audio;const e=new(window.AudioContext||window.webkitAudioContext),t=e.createGain(),n=e.createGain(),i=e.createMediaStreamDestination(),r=visibleTracks().filter(e=>"audio"===e.kind),a={};for(const t of r)a[t.id]=e.createGain();n.connect(e.destination),t.connect(n),t.connect(i);for(const e of Object.keys(a))a[e].connect(t);runtime.audio={ctx:e,master:t,monitor:n,recDest:i,trackBus:a,audioTrackIds:r.map(e=>e.id),meters:[],meterReady:!1},installMeterWorklet(runtime.audio).catch(()=>{});for(const[e,t]of runtime.clipEls){const n=getClip(e);n&&hookAudio(n,t)}return runtime.audio}function connectChannelIsolated(e,t,n,i){if(0===i||1===i){const r=e.createChannelSplitter(2),a=e.createChannelMerger(2);return t.connect(r),r.connect(n,i),n.connect(a,0,i),{out:a,split:r,merge:a}}return t.connect(n),{out:n,split:null,merge:null}}function hookAudio(e,t){if(runtime.audio&&!runtime.clipGain.has(e.id)&&("video"===e.kind||"audio"===e.kind))try{const n=runtime.audio.ctx,i=n.createMediaElementSource(t),r=n.createGain();r._fcSource=i,r.gain.value=0;const a=e.props?.audioChannel,{split:o,merge:s}=connectChannelIsolated(n,i,r,a);o&&(r._fcSplit=o,r._fcOut=s,r._fcChannel=a),runtime.clipGain.set(e.id,r),routeClipGain(e),t.volume=1}catch{}}function routeClipGain(e){const t=runtime.clipGain.get(e.id);if(!t||!runtime.audio)return;const n=runtime.audio.trackBus[e.track]||runtime.audio.master,i=t._fcOut||t;if(i._fcBus!==n){try{i.disconnect()}catch{}i.connect(n),i._fcBus=n}}const METER_SEGS=16,METER_DB_MIN=-48,METER_DB_MAX=0,METER_DB_MARKS=[0,-6,-12,-24,-36,-48],METER_MODES=["rms","lufs","peak"],METER_MODE_LABEL={rms:"RMS",lufs:"LUFS",peak:"PEAK"},METER_SEG_W=8,METER_SEG_H=12,METER_SEG_GAP=2,METER_COL_W=8,METER_COL_H=222;function makeMeterCanvas(e){const t=window.devicePixelRatio||1;e.style.width="8px",e.style.height="222px",e.width=Math.round(8*t),e.height=Math.round(222*t);const n=e.getContext("2d");return n.setTransform(t,0,0,t,0,0),{canvas:e,ctx:n}}function paintMeterSegs(e,t,n){const i=e.ctx;i.clearRect(0,0,8,222);for(let e=0;e<16;e++){const r=222-12*(e+1)-2*e,a=e<t||e===n,o=e/15;i.beginPath(),i.roundRect(0,r,8,12,1),a?(i.fillStyle=o<.6?"#3dd68c":o<.85?"#f0c14a":"#e5484d",i.fill()):(i.fillStyle="#2a2a33",i.fill(),i.strokeStyle="#0006",i.lineWidth=1,i.stroke())}}const meterState={mode:(()=>{try{const e=localStorage.getItem("fablecut-meter-mode");return METER_MODES.includes(e)?e:"rms"}catch{return"rms"}})(),trackIds:[],rms:{},peak:{},lufs:{},disp:{},peakHold:{},peakHoldT:{},segs:{},lastLit:{},lastHold:{},modeBtn:null};function audioMeterTracks(){return visibleTracks().filter(e=>"audio"===e.kind)}function cycleMeterMode(e){e&&(e.preventDefault(),e.stopPropagation());const t=METER_MODES.indexOf(meterState.mode);meterState.mode=METER_MODES[(t+1)%METER_MODES.length];try{localStorage.setItem("fablecut-meter-mode",meterState.mode)}catch{}meterState.modeBtn&&(meterState.modeBtn.textContent=METER_MODE_LABEL[meterState.mode]);const n=$("vuMeter");n&&(n.title=uiText("Mode: {mode} — click to switch",{mode:METER_MODE_LABEL[meterState.mode]}));for(const e of meterState.trackIds)meterState.disp[e]=METER_DB_MIN,meterState.peakHold[e]=METER_DB_MIN,meterState.peakHoldT[e]=0}async function installMeterWorklet(e){if(e.meterReady||!e.ctx.audioWorklet||e._meterLoading||!e.audioTrackIds.length)return;e._meterLoading=!0;const t=e.meterVersion||0,n=[];try{if(await e.ctx.audioWorklet.addModule("meter-worklet.js?v=2"),t!==(e.meterVersion||0))return;const i=e.audioTrackIds.slice();for(let t=0;t<i.length;t+=16){const r=i.slice(t,t+16),a=new AudioWorkletNode(e.ctx,"fablecut-meter",{numberOfInputs:r.length,numberOfOutputs:1,outputChannelCount:[2],channelCount:2,channelCountMode:"explicit",processorOptions:{hopBlocks:8,nTracks:r.length,trackIds:r}});n.push(a),a.port.onmessage=e=>{const t=e.data;if(t&&"meter"===t.type)for(let e=0;e<r.length;e++)meterState.rms[r[e]]=t.rms[e]||0,meterState.peak[r[e]]=t.peak[e]||0,meterState.lufs[r[e]]=t.lufs[e]??-70}}for(let t=0;t<i.length;t++){const r=e.trackBus[i[t]];r.disconnect(),r.connect(n[Math.floor(t/16)],0,t%16)}for(const t of n)t.connect(e.monitor),t.connect(e.recDest);e.meters=n,e.meterReady=!0;for(const e of i)meterState.rms[e]=0,meterState.peak[e]=0,meterState.lufs[e]=-70,meterState.disp[e]=METER_DB_MIN,meterState.peakHold[e]=METER_DB_MIN,meterState.peakHoldT[e]=0;buildMeterDOM()}catch(t){for(const e of n)e.port.onmessage=null,e.disconnect();for(const t of Object.values(e.trackBus))t.disconnect(),t.connect(e.master);console.warn("[FableCut] meter worklet unavailable:",t)}finally{e._meterLoading=!1,t!==(e.meterVersion||0)&&installMeterWorklet(e).catch(()=>{})}}function buildMeterDOM(){const e=$("vuMeter");if(!e)return;const t=audioMeterTracks();e.innerHTML="",e.title=uiText("Mode: {mode} — click to switch",{mode:METER_MODE_LABEL[meterState.mode]});const n=document.createElement("button");n.type="button",n.className="vu-mode",n.textContent=METER_MODE_LABEL[meterState.mode],n.title=uiText("Cycle RMS → LUFS → Peak"),n.addEventListener("click",cycleMeterMode),e.appendChild(n),meterState.modeBtn=n;const i=document.createElement("div");i.className="vu-channels";const r=document.createElement("div");r.className="vu-scale";const a=METER_DB_MAX-METER_DB_MIN;for(const e of METER_DB_MARKS){const t=document.createElement("span");t.className="vu-scale-tick",t.textContent=0===e?"0":String(e),t.style.top=(METER_DB_MAX-e)/a*100+"%",r.appendChild(t)}i.appendChild(r),meterState.segs={},meterState.lastLit={},meterState.lastHold={},meterState.trackIds=t.map(e=>e.id);for(const e of t){null==meterState.disp[e.id]&&(meterState.disp[e.id]=METER_DB_MIN,meterState.peakHold[e.id]=METER_DB_MIN,meterState.peakHoldT[e.id]=0,meterState.rms[e.id]=0,meterState.peak[e.id]=0,meterState.lufs[e.id]=-70);const t=document.createElement("div");t.className="vu-channel",t.dataset.track=e.id;const n=document.createElement("canvas");n.className="vu-segs";const r=makeMeterCanvas(n);meterState.segs[e.id]=r,paintMeterSegs(r,0,-1);const a=document.createElement("span");a.className="vu-label",a.textContent=e.id,t.appendChild(n),t.appendChild(a),i.appendChild(t)}e.appendChild(i)}function rmsToDb(e){return e>1e-8?20*Math.log10(e):METER_DB_MIN}function meterReadingDb(e){const t=meterState.mode;if("peak"===t)return rmsToDb(meterState.peak[e]||0);if("lufs"===t){const t=meterState.lufs[e];return null==t||t<METER_DB_MIN?METER_DB_MIN:Math.min(METER_DB_MAX,t)}return rmsToDb(meterState.rms[e]||0)}function updateMeterUI(e){const t=meterState.trackIds;if(!t.length)return;const n=(state.playing||state.audioHold)&&runtime.audio?.meterReady,i=meterState.mode,r="peak"===i?.005:"lufs"===i?.04:.015,a="peak"===i?.35:"lufs"===i?.12:.18,o=1-Math.exp(-e/r),s=1-Math.exp(-e/a),l=performance.now();for(const e of t){const t=n?meterReadingDb(e):METER_DB_MIN,i=meterState.disp[e]??METER_DB_MIN,r=i+(t-i)*(t>i?o:s);meterState.disp[e]=r;const a=t;a>=(meterState.peakHold[e]??METER_DB_MIN)?(meterState.peakHold[e]=a,meterState.peakHoldT[e]=l):l-(meterState.peakHoldT[e]||0)>800&&(meterState.peakHold[e]+=(METER_DB_MIN-meterState.peakHold[e])*s);const c=meterState.segs[e];if(!c)continue;const d=(r-METER_DB_MIN)/(METER_DB_MAX-METER_DB_MIN),u=Math.round(16*clamp(d,0,1)),p=Math.round(15*clamp(((meterState.peakHold[e]??METER_DB_MIN)-METER_DB_MIN)/(METER_DB_MAX-METER_DB_MIN),0,1));meterState.lastLit[e]===u&&meterState.lastHold[e]===p||(meterState.lastLit[e]=u,meterState.lastHold[e]=p,paintMeterSegs(c,u,p))}}let previewIntersecting=!0,previewPageActive=!0;function previewUnavailable(){return document.hidden||!previewPageActive||!previewIntersecting}function stopHiddenPreview(){previewUnavailable()&&(state.exporting||state.rendering||(pause(),silencePreviewMedia()),runtime.sfxPreview&&runtime.sfxPreview.pause(),els.libList.querySelectorAll(".lib-play").forEach(e=>{e.textContent="▶"}))}function play(){if(!previewUnavailable()&&(state.audioHold&&setAudioHold(!1),!state.playing)){if(ensureAudio(),runtime.audio.ctx.resume(),playLimited()){const{start:e,end:t}=playRange();state.time>=t-.01&&state.time<=t+.02&&(state.time=e)}else state.time>=projDur()-.01&&(state.time=0);resetPreviewWait(!0),lastTs=null,state.playing=!0,runtime.followPlayhead=!0,runtime.timelineScrollLeft=els.timelineScroll.scrollLeft,els.btnPlay.textContent="⏸",els.btnPlay.classList.add("on")}}function pause(){resetPreviewWait(),state.audioHold&&setAudioHold(!1),state.playing=!1,els.btnPlay.textContent="▶",els.btnPlay.classList.remove("on");for(const e of runtime.clipEls.values())e.paused||e.pause();state.exporting&&!backgroundExportActive&&finishExport(!1)}document.addEventListener("visibilitychange",stopHiddenPreview),window.addEventListener("pagehide",()=>{previewPageActive=!1,stopHiddenPreview()}),window.addEventListener("pageshow",()=>{previewPageActive=!0,stopHiddenPreview()}),window.parent!==window&&"undefined"!=typeof IntersectionObserver&&new IntersectionObserver(e=>{const t=e[e.length-1];t&&(previewIntersecting=t.isIntersecting&&t.intersectionRect.width>0&&t.intersectionRect.height>0,stopHiddenPreview())}).observe(document.documentElement);let audioHoldGen=0,audioHoldNodes=[],audioHoldRaf=0;function disposeAudioHoldNode(e){if(e){try{e.src.stop()}catch{}try{e.src.disconnect()}catch{}try{e.src&&(e.src.buffer=null)}catch{}try{e.gain.disconnect()}catch{}if(e.split)try{e.split.disconnect()}catch{}if(e.merge)try{e.merge.disconnect()}catch{}}}function stopAudioHoldNodes(){audioHoldGen++,audioHoldRaf&&(cancelAnimationFrame(audioHoldRaf),audioHoldRaf=0);for(const e of audioHoldNodes)disposeAudioHoldNode(e);audioHoldNodes=[]}function scheduleAudioHoldRefresh(){state.audioHold&&!state.playing&&(audioHoldRaf||(audioHoldRaf=requestAnimationFrame(()=>{audioHoldRaf=0,refreshAudioHold()})))}function sliceAudioFrame(e,t,n,i){const r=t.sampleRate,a=clamp(Math.floor(n*r),0,Math.max(0,t.length-1)),o=Math.max(1,Math.min(t.length-a,Math.round(i*r))),s=e.createBuffer(t.numberOfChannels,o,r);for(let e=0;e<t.numberOfChannels;e++)s.getChannelData(e).set(t.getChannelData(e).subarray(a,a+o));return s}function refreshAudioHold(){if(!state.audioHold||state.playing||state.exporting||state.rendering)return void stopAudioHoldNodes();const e=ensureAudio();try{e.ctx.resume()}catch{}const t=state.time,n=1/Math.max(1,project.fps||30),i=++audioHoldGen;for(const e of audioHoldNodes)disposeAudioHoldNode(e);audioHoldNodes=[];for(const e of runtime.clipEls.values())e.paused||e.pause();for(const e of runtime.clipGain.values())e.gain.value=0;for(const r of project.clips){if("audio"!==r.kind&&"video"!==r.kind)continue;if(!isTrackEnabled(r.track)||!activeAt(r,t))continue;const a=getMedia(r.mediaId);if(!a||"audio"!==a.kind&&"video"!==a.kind)continue;const o=evalProps(r,t),s=clamp(+o.volume||0,0,4);s<=1e-4||getAudioBuffer(a).then(a=>{if(i!==audioHoldGen||!state.audioHold||state.playing)return;const o=mediaTimeAt(r,t);if(!(a.duration>0)||o>=a.duration)return;const l=sliceAudioFrame(e.ctx,a,o,n),c=e.ctx.createBufferSource();c.buffer=l,c.loop=!0;const d=e.ctx.createGain();d.gain.value=s;const u=r.props?.audioChannel,{out:p,split:m,merge:f}=connectChannelIsolated(e.ctx,c,d,u),h=e.trackBus[r.track]||e.master;p.connect(h);const g={src:c,gain:d,split:m,merge:f};try{c.start(0)}catch{return void disposeAudioHoldNode(g)}i===audioHoldGen&&state.audioHold&&!state.playing?audioHoldNodes.push(g):disposeAudioHoldNode(g)}).catch(()=>{})}}function setAudioHold(e){if(e=!!e){if(state.exporting||state.rendering||previewUnavailable())return;if(resetPreviewWait(),state.playing){state.playing=!1,els.btnPlay.textContent="▶",els.btnPlay.classList.remove("on");for(const e of runtime.clipEls.values())e.paused||e.pause()}state.audioHold=!0,els.btnAudioHold&&els.btnAudioHold.classList.add("on"),refreshAudioHold()}else state.audioHold=!1,els.btnAudioHold&&els.btnAudioHold.classList.remove("on"),stopAudioHoldNodes()}const PREVIEW_RATES=[1,1.5,2,4];function playRate(){return state.exporting?1:state.previewRate}function setPreviewRate(e){state.previewRate=e,els.btnSpeed.textContent=e+"×",els.btnSpeed.classList.toggle("on",1!==e)}function cyclePreviewRate(e){const t=Math.max(0,PREVIEW_RATES.indexOf(state.previewRate));setPreviewRate(PREVIEW_RATES[(t+e+PREVIEW_RATES.length)%PREVIEW_RATES.length])}function stepPreviewRate(e){const t=Math.max(0,PREVIEW_RATES.indexOf(state.previewRate));setPreviewRate(PREVIEW_RATES[clamp(t+e,0,PREVIEW_RATES.length-1)])}function activeAt(e,t){return t>=e.start&&t<clipEnd(e)}function resetPreviewWait(e=!1){if(e&&(runtime.previewRemainder=0),e)for(const e of runtime.previewMedia.values())e.error=null;runtime.previewWait&&(els.btnPlay.textContent=state.playing?"⏸":"▶"),runtime.previewWait=null,els.btnPlay.title=uiText("Play / Pause (Space)")}function silencePreviewMedia(){for(const[e,t]of runtime.clipEls){t.paused||t.pause();const n=runtime.clipGain.get(e);n?n.gain.value=0:t.volume=0}}function previewNextTime(e,t){const n=state.time;if(!state.playing||runtime.scrub)return runtime.previewRemainder=0,n;if(state.exporting)return Math.min(t,n+e*playRate());if(runtime.audio&&"running"!==runtime.audio.ctx.state)return n;for(const e of project.clips){if("video"!==e.kind&&"audio"!==e.kind||!isTrackEnabled(e.track)||!activeAt(e,n))continue;const t=runtime.clipEls.get(e.id),i=runtime.previewMedia.get(e.id);if(!t||!i?.ready||i.playPending||t.paused&&!t.ended)return n}const i=Math.min(t,n+e*playRate()+(runtime.previewRemainder||0));let r=null;for(const e of project.clips)("video"===e.kind||"audio"===e.kind)&&isTrackEnabled(e.track)&&e.start>n&&e.start<=i&&(r=Math.max(r??n,e.start));const a=r??i;return runtime.previewRemainder=i-a,a}function previewFrameTime(e=state.time,t=projDur()){if(state.exporting&&!backgroundExportActive||state.rendering||t<=0||e<t)return e;const n=Math.max(1,project.fps||30);return(Math.max(1,Math.round(t*n))-1)/n}function preparePreviewMedia(e,t=projDur()){const n=previewFrameTime(e,t),i=n!==e;e=n;const r=new Set,a=[],o=2*playRate();for(const t of project.clips){if("video"!==t.kind&&"audio"!==t.kind||!isTrackEnabled(t.track))continue;const n=activeAt(t,e);if(!(n||t.start>e&&t.start<=e+o))continue;if(clipMediaFailure(t)){n&&a.push({c:t,error:mediaFailureLabel(clipMediaFailure(t))});continue}r.add(t.id);const s=runtime.clipEls.get(t.id);s&&s.getAttribute("src")!==mediaLoadSrc(getMedia(t.mediaId))&&releaseClipEl(t.id);const l=getClipEl(t);if(!l){n&&a.push({c:t,error:"missing media"});continue}const c=JSON.stringify([getMedia(t.mediaId)?.src,t.start,t.in,t.duration,t.props?.speed,t.keyframes?.speed]);let d=runtime.previewMedia.get(t.id);d&&d.key===c||(d={key:c,target:null,ready:!1,playPending:!1,error:null},runtime.previewMedia.set(t.id,d));const u=mediaTimeAt(t,n?e:t.start),p=clamp(+evalProps(t,n?e:t.start).speed||1,.1,8),m=clamp(p*playRate(),.0625,16);if(l.playbackRate!==m)try{l.playbackRate=m}catch{}if(!n){l.paused||l.pause();const e=runtime.clipGain.get(t.id);e?e.gain.value=0:l.volume=0}l.error&&(d.error=l.error.message||"media error "+l.error.code),!("video"===t.kind&&l.readyState>=1)||l.videoWidth&&l.videoHeight||(d.error="no decodable video track");const f=i?1e-4:state.playing&&!runtime.scrub&&!l.paused&&d.ready?.25*m:.04;if(l.readyState>=1&&!l.seeking&&(null==d.target||Math.abs(l.currentTime-u)>f)){d.target=u,d.ready=!1;try{Math.abs(l.currentTime-u)>1e-4&&(l.currentTime=u)}catch(e){d.error=e.message}}const h=!state.playing||runtime.scrub||i?2:3;d.ready=null!=d.target&&!l.seeking&&l.readyState>=h&&Math.abs(l.currentTime-u)<=f&&!d.error,n&&!d.ready&&a.push({c:t,error:d.error})}for(const e of[...runtime.clipEls.keys()])r.has(e)||releaseClipEl(e);return a}function syncMedia(){const e=state.time;for(const[t,n]of runtime.clipEls){const i=getClip(t),r=runtime.previewMedia.get(t);if(i&&state.playing&&!runtime.scrub&&(!runtime.audio||"running"===runtime.audio.ctx.state)&&isTrackEnabled(i.track)&&activeAt(i,e)&&r?.ready){const a=evalProps(i,e),o=clamp(a.volume,0,4),s=runtime.clipGain.get(t);s?s.gain.value=o:n.volume=clamp(o,0,1),n.paused&&!r.playPending&&(r.playPending=!0,n.play().catch(e=>{"AbortError"!==e.name&&runtime.previewMedia.get(t)===r&&(r.error=e.message||"playback failed")}).finally(()=>{r.playPending=!1}))}else{n.paused||n.pause();const e=runtime.clipGain.get(t);e?e.gain.value=0:n.volume=0}}}function seekMediaWhilePaused(){if(state.playing&&!runtime.scrub)return;const e=previewFrameTime();for(const t of project.clips){if("video"!==t.kind)continue;if(!isTrackEnabled(t.track))continue;if(!activeAt(t,e))continue;const n=getClipEl(t);if(!n)continue;const i=mediaTimeAt(t,e);if(Math.abs(n.currentTime-i)>(e!==state.time?1e-4:.04))try{n.currentTime=i}catch{}}}const EASE={linear:e=>e,"ease-in":e=>e*e,"ease-out":e=>1-(1-e)*(1-e),"ease-in-out":e=>e<.5?2*e*e:1-Math.pow(-2*e+2,2)/2};function evalProps(e,t){const n={...e.props},i=t-e.start;if(e.keyframes)for(const[t,r]of Object.entries(e.keyframes)){if(!Array.isArray(r)||!r.length)continue;let e;if(i<=r[0].t)e=r[0].v;else if(i>=r[r.length-1].t)e=r[r.length-1].v;else for(let t=0;t<r.length-1;t++){const n=r[t],a=r[t+1];if(i>=n.t&&i<=a.t){const t=(i-n.t)/Math.max(1e-6,a.t-n.t),r=EASE[a.ease||"ease-in-out"]||EASE.linear;e=n.v+(a.v-n.v)*r(t);break}}"number"!=typeof e||isNaN(e)||(n[t]=e)}applyFilterPreset(n);const r=els.preview.width,a=els.preview.height,o=e.transitionIn,s=e.transitionOut;return o&&o.duration>0&&i<o.duration&&applyTransition(n,o.type,1-EASE["ease-out"](clamp(i/o.duration,0,1)),r,a,-1),s&&s.duration>0&&i>e.duration-s.duration&&applyTransition(n,s.type,EASE["ease-in"](clamp((i-(e.duration-s.duration))/s.duration,0,1)),r,a,1),n}function applyFilterPreset(e){const t=FILTER_PRESETS[e.filterPreset];if(t&&"none"!==e.filterPreset)for(const[n,i]of Object.entries(t))e[n]="brightness"===n||"contrast"===n||"saturation"===n?(+e[n]||100)*i/100:"grayscale"===n||"sepia"===n||"vignette"===n||"invert"===n?clamp((+e[n]||0)+i,0,100):(+e[n]||0)+i}function applyTransition(e,t,n,i,r,a){if(n>0)switch(t){case"fade":case"dissolve":e.opacity*=1-n,e.volume*=1-n;break;case"slide-left":e.x=(+e.x||0)-a*n*i;break;case"slide-right":e.x=(+e.x||0)+a*n*i;break;case"slide-up":e.y=(+e.y||0)-a*n*r;break;case"slide-down":e.y=(+e.y||0)+a*n*r;break;case"zoom":e.scale=(+e.scale||1)*(1-.6*n),e.opacity*=1-n;break;case"wipe":case"wipe-left":e._wipe=n,e._wipeDir="left";break;case"wipe-right":e._wipe=n,e._wipeDir="right";break;case"wipe-up":e._wipe=n,e._wipeDir="up";break;case"wipe-down":e._wipe=n,e._wipeDir="down";break;case"iris":e._iris=n;break;case"spin":e.rotation=(+e.rotation||0)+a*n*200,e.scale=(+e.scale||1)*(1-.4*n),e.opacity*=1-n;break;case"blur":e.blur=(+e.blur||0)+24*n,e.opacity*=1-n*n,e.volume*=1-n;break;case"whip":e.x=(+e.x||0)-a*EASE["ease-in"](n)*i*1.4,e.blur=(+e.blur||0)+16*n;break;case"glitch":{const t=Math.sin(61.7*n)*Math.sin(23.3*n);e.rgbSplit=(+e.rgbSplit||0)+14*n,e.x=(+e.x||0)+t*n*i*.06,e.opacity*=1-n*n;break}case"pop":e.scale=(+e.scale||1)*Math.max(.001,backOut(1-n)),e.opacity*=Math.min(1,2.5*(1-n))}}function shiftKF(e,t,n){if(!e)return;const i={};for(const[r,a]of Object.entries(e)){const e=a.map(e=>({...e,t:+(e.t-t).toFixed(4)})).filter(e=>e.t>=-.001&&e.t<=n+.001);e.length&&(i[r]=e)}return Object.keys(i).length?i:void 0}function parseSvgSize(e){const t=e=>{const t=parseFloat(e);return isFinite(t)&&t>0?t:0},n=t=>(e.match(new RegExp(`<svg[^>]*\\s${t}="([^"%]+)"`,"i"))||[])[1];let i=t(n("width")),r=t(n("height"));if(!i||!r){const n=(e.match(/<svg[^>]*\sviewBox="([^"]+)"/i)||[])[1];if(n){const e=n.trim().split(/[\s,]+/);i=t(e[2]),r=t(e[3])}}return{width:i||800,height:r||600}}async function loadSvgMedia(e){const t=await fetch(e.src,{signal:AbortSignal.timeout(1e4)});if(!t.ok)throw new Error("SVG load failed: "+t.status);const n=await t.text(),{width:i,height:r}=parseSvgSize(n);e.width=i,e.height=r;const a=runtime.mediaAux.get(e.id)||{};a.svgText=n,a.svgAnimated=/@keyframes|animation\s*:/i.test(n),a.svgFrames=new Map,a.svgPending=null,runtime.mediaAux.set(e.id,a),a.svgAnimated||(a.img=await loadImage(e.src)),state.dirtyTimeline=!0}function svgUrlAt(e,t){const n=`<style>*{animation-play-state:paused!important;animation-delay:calc(var(--d,0s) - ${t.toFixed(4)}s)!important}</style>`,i=e.svgText.replace(/(<svg[^>]*>)/i,`$1${n}`);return"data:image/svg+xml;charset=utf-8,"+encodeURIComponent(i)}function renderSvgFrame(e,t){return new Promise((n,i)=>{const r=new Image;r.onload=()=>n(r),r.onerror=()=>i(new Error("svg raster failed")),r.src=svgUrlAt(e,t)})}function getSvgImage(e,t){const n=runtime.mediaAux.get(e.mediaId);if(!n||!n.svgText)return null;if(!n.svgAnimated)return n.img||null;const i=Math.max(0,mediaTimeAt(e,t)),r=Math.round(i*project.fps)/project.fps;return n.svgFrames.get(r)||(n.svgPending||(n.svgPending=renderSvgFrame(n,r).then(e=>{n.svgFrames.set(r,e),n.svgFrames.size>90&&n.svgFrames.delete(n.svgFrames.keys().next().value),n.lastImg=e}).catch(()=>{}).finally(()=>{n.svgPending=null})),n.lastImg||null)}async function prepareSvgFrame(e,t){const n=runtime.mediaAux.get(e.mediaId);if(!n)return;if(!n.svgText)try{await loadSvgMedia(getMedia(e.mediaId))}catch{return}if(!n.svgAnimated)return;const i=Math.max(0,mediaTimeAt(e,t)),r=Math.round(i*project.fps)/project.fps;if(!n.svgFrames.get(r))try{const e=await renderSvgFrame(n,r);n.svgFrames.set(r,e),n.svgFrames.size>90&&n.svgFrames.delete(n.svgFrames.keys().next().value),n.lastImg=e}catch{}}const bgSeg={seg:null,loading:null,failed:!1,queue:Promise.resolve(),masks:new Map};function loadScript(e){return new Promise((t,n)=>{const i=document.createElement("script");i.src=e,i.onload=t,i.onerror=()=>n(new Error("script load failed: "+e)),document.head.appendChild(i)})}function ensureBgSeg(){if(bgSeg.seg||bgSeg.failed)return bgSeg.loading||Promise.resolve();if(!bgSeg.loading){const e="https://cdn.jsdelivr.net/npm/@mediapipe/selfie_segmentation";bgSeg.loading=loadScript(`${e}/selfie_segmentation.js`).then(()=>{const t=new SelfieSegmentation({locateFile:t=>`${e}/${t}`});t.setOptions({modelSelection:1}),t.onResults(e=>{if(!bgSeg.currentClip)return;let t=bgSeg.masks.get(bgSeg.currentClip);t||(t=document.createElement("canvas"),bgSeg.masks.set(bgSeg.currentClip,t)),t.width=e.segmentationMask.width,t.height=e.segmentationMask.height,t.getContext("2d").drawImage(e.segmentationMask,0,0)}),bgSeg.seg=t}).catch(()=>{bgSeg.failed=!0,toast(uiText("Background removal unavailable — couldn't load MediaPipe (offline?). Using chroma key still works."))})}return bgSeg.loading}function requestMask(e,t,n=!1){return ensureBgSeg(),bgSeg.seg?!n&&bgSeg.pending>0?Promise.resolve():(bgSeg.pending++,bgSeg.queue=bgSeg.queue.then(async()=>{if(0!==(t.videoWidth||t.naturalWidth||t.width||0)){bgSeg.currentClip=e;try{await bgSeg.seg.send({image:t})}catch{}}}).finally(()=>{bgSeg.pending--}),bgSeg.queue):bgSeg.loading||Promise.resolve()}bgSeg.pending=0;const scratch=document.createElement("canvas"),scratchCtx=scratch.getContext("2d",{willReadFrequently:!0});let grainTile=null;function getGrainTile(){if(grainTile)return grainTile;grainTile=document.createElement("canvas"),grainTile.width=grainTile.height=256;const e=grainTile.getContext("2d"),t=e.createImageData(256,256);for(let e=0;e<t.data.length;e+=4){const n=90+130*Math.random();t.data[e]=t.data[e+1]=t.data[e+2]=n,t.data[e+3]=255}return e.putImageData(t,0,0),grainTile}function drawGrain(e,t,n,i,r,a){const o=ctx2d.globalAlpha,s=ctx2d.globalCompositeOperation,l=7919*Math.floor(a*project.fps)%256;ctx2d.globalCompositeOperation="overlay",ctx2d.globalAlpha=o*clamp(e/100,0,1)*.55,ctx2d.save(),ctx2d.translate(-l,l),ctx2d.fillStyle=ctx2d.createPattern(getGrainTile(),"repeat"),ctx2d.fillRect(t+l,n-l,i,r),ctx2d.restore(),ctx2d.globalAlpha=o,ctx2d.globalCompositeOperation=s}const adjScratch=document.createElement("canvas");function drawAdjust(e,t,n,i){const r=evalProps(e,i);adjScratch.width!==t&&(adjScratch.width=t),adjScratch.height!==n&&(adjScratch.height=n);const a=adjScratch.getContext("2d");if(a.clearRect(0,0,t,n),a.drawImage(els.preview,0,0),ctx2d.save(),ctx2d.setTransform(1,0,0,1,0,0),r.shake>0){const e=+r.shake,t=i*clamp(+r.shakeSpeed||8,.5,40)*Math.PI*2;ctx2d.translate(Math.sin(1.3*t)*e*.6+Math.sin(2.71*t)*e*.4,Math.cos(1.7*t)*e*.5+Math.sin(3.13*t)*e*.35)}ctx2d.globalAlpha=clamp(r.opacity,0,1),ctx2d.filter=buildFilter(r);let o=adjScratch;if((r.temperature||r.tint||r.rgbSplit>0)&&(o=pixelPass(e,{...r,chromaKey:"",bgRemove:!1},adjScratch,0,0,t,n,t,n)),ctx2d.drawImage(o,0,0,o.width,o.height,0,0,t,n),ctx2d.filter="none",r.vignette>0){const e=ctx2d.createRadialGradient(t/2,n/2,.35*Math.min(t,n),t/2,n/2,.55*Math.hypot(t,n));e.addColorStop(0,"rgba(0,0,0,0)"),e.addColorStop(1,`rgba(0,0,0,${.9*clamp(r.vignette/100,0,1)})`),ctx2d.fillStyle=e,ctx2d.fillRect(0,0,t,n)}r.grain>0&&drawGrain(r.grain,0,0,t,n,i),ctx2d.restore()}function needsPixelPass(e,t){return!!(e.chromaKey||e.temperature||e.tint||e.rgbSplit>0||e.bgRemove&&bgSeg.masks.get(t.id))}function hexToRgb(e){const t=parseInt(String(e).replace("#",""),16)||0;return[t>>16&255,t>>8&255,255&t]}const pixGL={tried:!1,gl:null,canvas:null,loc:null,stage:null,stageCtx:null,outs:new Map},PIX_VERT="#version 300 es\nin vec2 aPos;\nvoid main() { gl_Position = vec4(aPos, 0.0, 1.0); }",PIX_FRAG="#version 300 es\nprecision highp float;\nuniform sampler2D uTex;\nuniform ivec2 uSize;\nuniform int uSplit;\nuniform int uKey;\nuniform vec2 uKeyC;\nuniform vec2 uKeyT;\nuniform vec2 uShift;\nout vec4 o;\nvoid main() {\n ivec2 sz = uSize;\n ivec2 q = ivec2(int(gl_FragCoord.x), sz.y - 1 - int(gl_FragCoord.y));\n vec4 c = texelFetch(uTex, q, 0);\n vec3 rgb = c.rgb * 255.0;\n float a = c.a;\n if (uSplit > 0) {\n rgb.r = texelFetch(uTex, ivec2(min(sz.x - 1, q.x + uSplit), q.y), 0).r * 255.0;\n rgb.b = texelFetch(uTex, ivec2(max(0, q.x - uSplit), q.y), 0).b * 255.0;\n }\n if (uKey == 1) {\n float cb = 128.0 - 0.168736 * rgb.r - 0.331264 * rgb.g + 0.5 * rgb.b;\n float cr = 128.0 + 0.5 * rgb.r - 0.418688 * rgb.g - 0.081312 * rgb.b;\n float dist = distance(vec2(cb, cr), uKeyC);\n if (dist < uKeyT.x) { o = vec4(0.0); return; }\n if (dist < uKeyT.y) {\n float k = (dist - uKeyT.x) / (uKeyT.y - uKeyT.x);\n a *= k;\n float avg = (rgb.r + rgb.b) * 0.5;\n if (rgb.g > avg) rgb.g = rgb.g * k + avg * (1.0 - k);\n }\n }\n rgb += vec3(uShift.x, uShift.y, -uShift.x);\n rgb = clamp(rgb, 0.0, 255.0) / 255.0;\n o = vec4(rgb * a, a);\n}";function initPixGL(){if(pixGL.tried)return pixGL.gl;pixGL.tried=!0;try{const e=document.createElement("canvas"),t=e.getContext("webgl2",{premultipliedAlpha:!0,preserveDrawingBuffer:!0,antialias:!1,alpha:!0});if(!t)return null;const n=t.getExtension("WEBGL_debug_renderer_info");if(n&&/swiftshader|llvmpipe|softpipe|software/i.test(t.getParameter(n.UNMASKED_RENDERER_WEBGL)))return null;const i=(e,n)=>{const i=t.createShader(e);if(t.shaderSource(i,n),t.compileShader(i),!t.getShaderParameter(i,t.COMPILE_STATUS))throw new Error(t.getShaderInfoLog(i));return i},r=t.createProgram();if(t.attachShader(r,i(t.VERTEX_SHADER,PIX_VERT)),t.attachShader(r,i(t.FRAGMENT_SHADER,PIX_FRAG)),t.linkProgram(r),!t.getProgramParameter(r,t.LINK_STATUS))throw new Error(t.getProgramInfoLog(r));t.useProgram(r),t.bindBuffer(t.ARRAY_BUFFER,t.createBuffer()),t.bufferData(t.ARRAY_BUFFER,new Float32Array([-1,-1,1,-1,-1,1,1,1]),t.STATIC_DRAW);const a=t.getAttribLocation(r,"aPos");t.enableVertexAttribArray(a),t.vertexAttribPointer(a,2,t.FLOAT,!1,0,0),t.bindTexture(t.TEXTURE_2D,t.createTexture());for(const[e,n]of[[t.TEXTURE_MIN_FILTER,t.NEAREST],[t.TEXTURE_MAG_FILTER,t.NEAREST],[t.TEXTURE_WRAP_S,t.CLAMP_TO_EDGE],[t.TEXTURE_WRAP_T,t.CLAMP_TO_EDGE]])t.texParameteri(t.TEXTURE_2D,e,n);t.pixelStorei(t.UNPACK_FLIP_Y_WEBGL,!1),t.pixelStorei(t.UNPACK_PREMULTIPLY_ALPHA_WEBGL,!1);const o={};for(const e of["uTex","uSize","uSplit","uKey","uKeyC","uKeyT","uShift"])o[e]=t.getUniformLocation(r,e);t.uniform1i(o.uTex,0),e.addEventListener("webglcontextlost",e=>{e.preventDefault(),pixGL.gl=null}),e.addEventListener("webglcontextrestored",()=>{pixGL.tried=!1});const s=document.createElement("canvas");return Object.assign(pixGL,{gl:t,canvas:e,loc:o,stage:s,stageCtx:s.getContext("2d")}),t}catch(e){return console.warn("[FableCut] WebGL2 pixel pass unavailable, using the CPU path:",e&&e.message),null}}function pixelPassGL(e,t,n,i,r,a,o,s,l,c){const d=initPixGL();if(!d)return null;const{stage:u,stageCtx:p,canvas:m,loc:f}=pixGL;u.width<s&&(u.width=s),u.height<l&&(u.height=l),p.clearRect(0,0,s,l),p.drawImage(n,i,r,a,o,0,0,s,l);const h=t.bgRemove?bgSeg.masks.get(e.id):null;h&&h.width&&(p.globalCompositeOperation="destination-in",p.drawImage(h,0,0,s,l),p.globalCompositeOperation="source-over");const g=!!t.chromaKey,v=+t.temperature||0,x=+t.tint||0,b=Math.round(clamp(+t.rgbSplit||0,0,60)*(s/Math.max(1,c))),y=g||v||x||b>0;let T=pixGL.outs.get(e.id);T||(pixGL.outs.size>=32&&pixGL.outs.delete(pixGL.outs.keys().next().value),T=document.createElement("canvas"),pixGL.outs.set(e.id,T)),T.width!==s&&(T.width=s),T.height!==l&&(T.height=l);const k=T.getContext("2d");if(k.clearRect(0,0,s,l),!y)return k.drawImage(u,0,0,s,l,0,0,s,l),T;try{if(m.width<s&&(m.width=s),m.height<l&&(m.height=l),d.viewport(0,0,s,l),d.texImage2D(d.TEXTURE_2D,0,d.RGBA,d.RGBA,d.UNSIGNED_BYTE,u),d.uniform2i(f.uSize,s,l),d.uniform1i(f.uSplit,b),d.uniform1i(f.uKey,g?1:0),g){const[e,n,i]=hexToRgb(t.chromaKey),r=1.2*clamp(+t.chromaTolerance||0,0,100);d.uniform2f(f.uKeyC,128-.168736*e-.331264*n+.5*i,128+.5*e-.418688*n-.081312*i),d.uniform2f(f.uKeyT,r,r+Math.max(1,1.2*clamp(+t.chromaSoftness||0,0,100)))}return d.uniform2f(f.uShift,.6*v,.5*x),d.clearColor(0,0,0,0),d.clear(d.COLOR_BUFFER_BIT),d.drawArrays(d.TRIANGLE_STRIP,0,4),k.drawImage(m,0,m.height-l,s,l,0,0,s,l),T}catch(e){if(e&&"SecurityError"===e.name)throw e;return console.warn("[FableCut] pixel pass failed, using the CPU path:",e&&e.message),pixGL.gl=null,null}}function pixelPass(e,t,n,i,r,a,o,s,l){const c=Math.max(2,Math.min(Math.round(s),1920)),d=Math.max(2,Math.min(Math.round(l),1920)),u=pixelPassGL(e,t,n,i,r,a,o,c,d,s);if(u)return u;scratch.width!==c&&(scratch.width=c),scratch.height!==d&&(scratch.height=d),scratchCtx.clearRect(0,0,c,d),scratchCtx.drawImage(n,i,r,a,o,0,0,c,d);const p=t.bgRemove?bgSeg.masks.get(e.id):null;p&&p.width&&(scratchCtx.globalCompositeOperation="destination-in",scratchCtx.drawImage(p,0,0,c,d),scratchCtx.globalCompositeOperation="source-over");const m=!!t.chromaKey,f=+t.temperature||0,h=+t.tint||0,g=Math.round(clamp(+t.rgbSplit||0,0,60)*(c/Math.max(1,s)));if(m||f||h||g>0){const e=scratchCtx.getImageData(0,0,c,d),n=e.data;if(g>0){const e=new Uint8ClampedArray(n);for(let t=0;t<d;t++){const i=t*c*4;for(let t=0;t<c;t++){const r=i+4*t;n[r]=e[i+4*Math.min(c-1,t+g)],n[r+2]=e[i+4*Math.max(0,t-g)+2]}}}let i=0,r=0,a=0,o=1;if(m){const[e,n,s]=hexToRgb(t.chromaKey);i=128-.168736*e-.331264*n+.5*s,r=128+.5*e-.418688*n-.081312*s,a=1.2*clamp(+t.chromaTolerance||0,0,100),o=a+Math.max(1,1.2*clamp(+t.chromaSoftness||0,0,100))}const s=.6*f,l=.5*h;for(let e=0;e<n.length;e+=4){let t=n[e],c=n[e+1],d=n[e+2];if(m){const s=128-.168736*t-.331264*c+.5*d,l=128+.5*t-.418688*c-.081312*d,u=Math.sqrt((s-i)*(s-i)+(l-r)*(l-r));if(u<a){n[e+3]=0;continue}if(u<o){const i=(u-a)/(o-a);n[e+3]=Math.round(n[e+3]*i);const r=(t+d)/2;c>r&&(c=c*i+r*(1-i))}}s&&(t+=s,d-=s),l&&(c+=l),n[e]=clamp(t,0,255),n[e+1]=clamp(c,0,255),n[e+2]=clamp(d,0,255)}scratchCtx.putImageData(e,0,0)}return scratch}function buildFilter(e){const t=[];return 100!==e.brightness&&t.push(`brightness(${e.brightness}%)`),100!==e.contrast&&t.push(`contrast(${e.contrast}%)`),100!==e.saturation&&t.push(`saturate(${e.saturation}%)`),e.hue&&t.push(`hue-rotate(${e.hue}deg)`),e.blur&&t.push(`blur(${e.blur}px)`),e.grayscale&&t.push(`grayscale(${e.grayscale}%)`),e.sepia&&t.push(`sepia(${e.sepia}%)`),e.invert&&t.push(`invert(${e.invert}%)`),t.length?t.join(" "):"none"}function visibleClipsAt(e){const t=[],n=TRACKS.filter(e=>"video"===e.kind&&isTrackEnabled(e.id)).reverse();for(const i of n)t.push(...project.clips.filter(t=>t.track===i.id&&activeAt(t,e)).sort((e,t)=>e.start-t.start));return t}function drawFrame(e=state.time){const t=els.preview.width,n=els.preview.height;ctx2d.setTransform(1,0,0,1,0,0),ctx2d.filter="none",ctx2d.globalAlpha=1,ctx2d.fillStyle=project.background||"#000",ctx2d.fillRect(0,0,t,n);for(const i of visibleClipsAt(e))drawClip(i,t,n,e);state.exporting||state.playing||drawSelectionOverlay(t,n,e)}const CANVAS_TRANSFORM_ENABLED=!1;function clipBounds(e,t,n,i){const r=n/2+(+t.x||0),a=i/2+(+t.y||0),o=(t.rotation||0)*Math.PI/180,s=+t.scale||1;let l,c;if("text"===e.kind)if(hasTextBox(t))l=+t.boxW/2,c=+t.boxH/2;else{const e=measureTextHalfSize(t);l=e.hw,c=e.hh}else l=n/2*s,c=i/2*s;return{cx:r,cy:a,hw:l,hh:c,rot:o}}function isVisualClip(e){return e&&"adjust"!==e.kind&&"audio"!==e.kind}function overlayHandles(e,t,n){const i=Math.cos(e.rot),r=Math.sin(e.rot),a=(t,n)=>({x:e.cx+t*i-n*r,y:e.cy+t*r+n*i}),o=Math.max(6,t/150),s=Math.max(24,t/34),l=1.4*o,c=e=>({x:clamp(e.x,l,t-l),y:clamp(e.y,l,n-l)});return{hs:o,corners:[[-e.hw,-e.hh],[e.hw,-e.hh],[e.hw,e.hh],[-e.hw,e.hh]].map(([e,t])=>c(a(e,t))),topMid:c(a(0,-e.hh)),rotate:c(a(0,-e.hh-s))}}function drawSelectionOverlay(e,t,n){const i=getClip(state.selId);if(!isVisualClip(i)||!activeAt(i,n)||!isTrackEnabled(i.track))return;const r=clipBounds(i,evalProps(i,n),e,t),a=Math.max(2,e/640),o=overlayHandles(r,e,t),s=o.hs;if(ctx2d.setTransform(1,0,0,1,0,0),ctx2d.save(),ctx2d.lineWidth=a,ctx2d.strokeStyle="#4f8cff",ctx2d.save(),ctx2d.translate(r.cx,r.cy),ctx2d.rotate(r.rot),ctx2d.setLineDash([4*a,3*a]),ctx2d.strokeRect(-r.hw,-r.hh,2*r.hw,2*r.hh),ctx2d.restore(),ctx2d.setLineDash([]),CANVAS_TRANSFORM_ENABLED){ctx2d.beginPath(),ctx2d.moveTo(o.topMid.x,o.topMid.y),ctx2d.lineTo(o.rotate.x,o.rotate.y),ctx2d.stroke(),ctx2d.fillStyle="#ffffff";for(const e of o.corners)ctx2d.beginPath(),ctx2d.rect(e.x-s,e.y-s,2*s,2*s),ctx2d.fill(),ctx2d.stroke();ctx2d.beginPath(),ctx2d.arc(o.rotate.x,o.rotate.y,1.1*s,0,2*Math.PI),ctx2d.fillStyle="#ffce5c",ctx2d.fill(),ctx2d.stroke()}ctx2d.restore()}function canvasPt(e){const t=els.preview.getBoundingClientRect();return{x:(e.clientX-t.left)*(els.preview.width/t.width),y:(e.clientY-t.top)*(els.preview.height/t.height)}}function toLocal(e,t){const n=e.x-t.cx,i=e.y-t.cy,r=Math.cos(-t.rot),a=Math.sin(-t.rot);return{x:n*r-i*a,y:n*a+i*r}}function pickClipAt(e,t,n){const i=visibleClipsAt(state.time).filter(isVisualClip);for(let r=i.length-1;r>=0;r--){const a=i[r],o=clipBounds(a,evalProps(a,state.time),t,n),s=toLocal(e,o);if(Math.abs(s.x)<=o.hw&&Math.abs(s.y)<=o.hh)return a}return null}let canvasDrag=null,canvasDidMove=!1;els.preview.style.touchAction="none",els.preview.addEventListener("pointerdown",e=>{if(e.altKey||1===e.button)return;const t=els.preview.width,n=els.preview.height,i=canvasPt(e),r=getClip(state.selId),a=pickClipAt(i,t,n);if(canvasDrag=null,CANVAS_TRANSFORM_ENABLED&&isVisualClip(r)&&activeAt(r,state.time)){const e=clipBounds(r,evalProps(r,state.time),t,n),o=toLocal(i,e),s=overlayHandles(e,t,n),l=1.8*s.hs;if(Math.hypot(i.x-s.rotate.x,i.y-s.rotate.y)<=l)canvasDrag={mode:"rotate",id:r.id,startRot:+r.props.rotation||0,startAng:Math.atan2(i.y-e.cy,i.x-e.cx)};else if(s.corners.some(e=>Math.abs(i.x-e.x)<=l&&Math.abs(i.y-e.y)<=l))if("text"===r.kind){ensureTextBox(r);const e=clipBounds(r,evalProps(r,state.time),t,n),a=overlayHandles(e,t,n).corners.findIndex(e=>Math.abs(i.x-e.x)<=l&&Math.abs(i.y-e.y)<=l),o=[[-1,-1],[1,-1],[1,1],[-1,1]],[s,c]=o[a>=0?a:0],d=Math.cos(e.rot),u=Math.sin(e.rot),p=-s*e.hw,m=-c*e.hh;canvasDrag={mode:"box",id:r.id,rot:e.rot,dragSX:s,dragSY:c,fix:{x:e.cx+p*d-m*u,y:e.cy+p*u+m*d},aspect:Math.max(.05,2*e.hw/Math.max(1e-6,2*e.hh))}}else canvasDrag={mode:"scale",id:r.id,startScale:+r.props.scale||1,startDist:Math.hypot(o.x,o.y)||1};else a?.id===r.id&&Math.abs(o.x)<=e.hw&&Math.abs(o.y)<=e.hh&&(canvasDrag={mode:"move",id:r.id,startX:+r.props.x||0,startY:+r.props.y||0,startPt:i})}if(!canvasDrag){if(!a)return;a.id!==state.selId&&(selectClip(a.id),renderInspector()),canvasDrag={mode:CANVAS_TRANSFORM_ENABLED?"move":"select",id:a.id,startX:+a.props.x||0,startY:+a.props.y||0,startPt:i}}canvasDrag.clientX=e.clientX,canvasDrag.clientY=e.clientY,canvasDrag.editText=0===e.button&&!state.playing&&!state.exporting,canvasDidMove=!1,"move"===canvasDrag.mode?els.preview.style.cursor="move":"rotate"===canvasDrag.mode&&(els.preview.style.cursor=ROTATE_CURSOR),els.preview.setPointerCapture(e.pointerId),e.preventDefault()});const ROTATE_CURSOR=`url("data:image/svg+xml,${encodeURIComponent("<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'><path d='M10 3a7 7 0 1 1-6.7 9' fill='none' stroke='black' stroke-width='4.5' stroke-linecap='round'/><path d='M10 3a7 7 0 1 1-6.7 9' fill='none' stroke='white' stroke-width='2' stroke-linecap='round'/><path d='M10.5 0.5L14.5 3L10.5 5.5Z' fill='white' stroke='black'/></svg>")}") 10 10, crosshair`;function cursorForHandleAngle(e){return["ew-resize","nwse-resize","ns-resize","nesw-resize"][(Math.round(e/45)%4+4)%4]}function updateCanvasCursor(e){const t=els.preview.width,n=els.preview.height,i=canvasPt(e),r=getClip(state.selId);let a="default";if(isVisualClip(r)&&activeAt(r,state.time)&&!state.playing&&!state.exporting){const e=clipBounds(r,evalProps(r,state.time),t,n),o=overlayHandles(e,t,n),s=1.8*o.hs,l=o.corners.find(e=>Math.abs(i.x-e.x)<=s&&Math.abs(i.y-e.y)<=s),c=toLocal(i,e);CANVAS_TRANSFORM_ENABLED&&Math.hypot(i.x-o.rotate.x,i.y-o.rotate.y)<=s?a=ROTATE_CURSOR:CANVAS_TRANSFORM_ENABLED&&l?a=cursorForHandleAngle(180*Math.atan2(l.y-e.cy,l.x-e.cx)/Math.PI):CANVAS_TRANSFORM_ENABLED&&Math.abs(c.x)<=e.hw&&Math.abs(c.y)<=e.hh?a="move":pickClipAt(i,t,n)&&(a="pointer")}else pickClipAt(i,t,n)&&(a="pointer");els.preview.style.cursor=a}function endCanvasDrag(e){if(!canvasDrag)return;const t=!canvasDidMove&&("move"===canvasDrag.mode||"select"===canvasDrag.mode)&&canvasDrag.editText&&"pointerup"===e.type&&!state.playing&&!state.exporting&&"text"===getClip(canvasDrag.id)?.kind,n=canvasDrag.id,i=canvasDidMove&&"select"!==canvasDrag.mode;canvasDrag=null;try{els.preview.releasePointerCapture(e.pointerId)}catch{}i&&(scheduleSave(),renderInspector()),t&&(selectClip(n),renderInspector(),focusTextContent()),updateCanvasCursor(e)}function drawClip(e,t,n,i){if("adjust"===e.kind)return void drawAdjust(e,t,n,i);const r=evalProps(e,i);if(ctx2d.save(),r._wipe){ctx2d.beginPath();const e=r._wipe;"right"===r._wipeDir?ctx2d.rect(t*e,0,t*(1-e),n):"up"===r._wipeDir?ctx2d.rect(0,0,t,n*(1-e)):"down"===r._wipeDir?ctx2d.rect(0,n*e,t,n*(1-e)):ctx2d.rect(0,0,t*(1-e),n),ctx2d.clip()}if(null!=r._iris&&(ctx2d.beginPath(),ctx2d.arc(t/2,n/2,Math.max(.01,1-r._iris)*Math.hypot(t,n)*.55,0,2*Math.PI),ctx2d.clip()),ctx2d.globalAlpha=clamp(r.opacity,0,1),r.blend&&"normal"!==r.blend&&BLEND_MODES.includes(r.blend)&&(ctx2d.globalCompositeOperation="normal"===r.blend?"source-over":r.blend),ctx2d.translate(t/2+(+r.x||0),n/2+(+r.y||0)),ctx2d.rotate((r.rotation||0)*Math.PI/180),r.shake>0){const e=+r.shake,t=i*clamp(+r.shakeSpeed||8,.5,40)*Math.PI*2;ctx2d.translate(Math.sin(1.3*t)*e*.6+Math.sin(2.71*t)*e*.4,Math.cos(1.7*t)*e*.5+Math.sin(3.13*t)*e*.35),ctx2d.rotate(Math.sin(.9*t)*e*.0022)}if("text"===e.kind)return drawText(e,r,i-e.start),void ctx2d.restore();let a=null,o=0,s=0;if("image"===e.kind?(a=runtime.mediaAux.get(e.mediaId)?.img,a&&(o=a.naturalWidth,s=a.naturalHeight)):"svg"===e.kind?(a=getSvgImage(e,i),a&&(o=a.naturalWidth||a.width,s=a.naturalHeight||a.height)):"video"===e.kind&&(a=optimizedSources?.get(e.id)||exportDecodedFrames.get(e.id)||getClipEl(e),exportDecodedFrames.has(e.id)&&!optimizedSources?.has(e.id)?(o=a.displayWidth,s=a.displayHeight):optimizedSources?.has(e.id)?(o=a.naturalWidth||a.width,s=a.naturalHeight||a.height):a&&!a.seeking&&a.readyState>=2&&(o=a.videoWidth,s=a.videoHeight)),a&&o&&s){const l=o*clamp(+r.cropL||0,0,95)/100,c=s*clamp(+r.cropT||0,0,95)/100,d=Math.max(1,o-l-o*clamp(+r.cropR||0,0,95)/100),u=Math.max(1,s-c-s*clamp(+r.cropB||0,0,95)/100),p=r.scale||1;let m,f;if("cover"===r.fit){const e=Math.max(t/d,n/u)*p;m=d*e,f=u*e}else if("stretch"===r.fit)m=t*p,f=n*p;else if("none"===r.fit)m=d*p,f=u*p;else{const e=Math.min(t/d,n/u)*p;m=d*e,f=u*e}if((r.flipH||r.flipV)&&ctx2d.scale(r.flipH?-1:1,r.flipV?-1:1),r.cornerRadius>0&&(ctx2d.beginPath(),ctx2d.roundRect(-m/2,-f/2,m,f,Math.min(+r.cornerRadius,m/2,f/2)),ctx2d.clip()),r.bgRemove&&"video"===e.kind&&!optimizedSources&&requestMask(e.id,a),r.bgRemove&&"image"===e.kind&&!bgSeg.masks.get(e.id)&&requestMask(e.id,a),ctx2d.filter=buildFilter(r),needsPixelPass(r,e)){const t=pixelPass(e,r,a,l,c,d,u,m,f);ctx2d.drawImage(t,0,0,t.width,t.height,-m/2,-f/2,m,f)}else ctx2d.drawImage(a,l,c,d,u,-m/2,-f/2,m,f);if(ctx2d.filter="none",r.vignette>0){const e=ctx2d.createRadialGradient(0,0,.35*Math.min(m,f),0,0,.55*Math.hypot(m,f));e.addColorStop(0,"rgba(0,0,0,0)"),e.addColorStop(1,`rgba(0,0,0,${.9*clamp(r.vignette/100,0,1)})`),ctx2d.fillStyle=e,ctx2d.fillRect(-m/2,-f/2,m,f)}r.grain>0&&drawGrain(r.grain,-m/2,-f/2,m,f,i)}ctx2d.restore()}let _textDirProbe;els.preview.addEventListener("pointermove",e=>{if(!canvasDrag)return void updateCanvasCursor(e);const t=getClip(canvasDrag.id);if(!t)return;const n=els.preview.width,i=els.preview.height,r=canvasPt(e);if(canvasDidMove||!(Math.hypot(e.clientX-canvasDrag.clientX,e.clientY-canvasDrag.clientY)<3))if("select"!==canvasDrag.mode)if(canvasDidMove||(pushUndo(),canvasDidMove=!0),"move"===canvasDrag.mode)t.props.x=Math.round(canvasDrag.startX+(r.x-canvasDrag.startPt.x)),t.props.y=Math.round(canvasDrag.startY+(r.y-canvasDrag.startPt.y));else if("box"===canvasDrag.mode){const a=canvasDrag.aspect||1,o=e.shiftKey;if(e.ctrlKey||e.metaKey){const e=toLocal(r,clipBounds(t,evalProps(t,state.time),n,i));let s=2*Math.abs(e.x),l=2*Math.abs(e.y);o&&(s/a>=l?l=s/a:s=l*a),t.props.boxW=+clamp(s,20,3*n).toFixed(1),t.props.boxH=+clamp(l,16,3*i).toFixed(1)}else{const e=canvasDrag.fix,s=canvasDrag.rot;let l=r.x-e.x,c=r.y-e.y;const d=Math.cos(-s),u=Math.sin(-s);let p=l*d-c*u,m=l*u+c*d;if(o){const e=Math.abs(p),t=Math.abs(m);e/a>=t?m=(Math.sign(m)||canvasDrag.dragSY)*(e/a):p=(Math.sign(p)||canvasDrag.dragSX)*(t*a)}const f=20,h=16;Math.abs(p)<f&&(p=(Math.sign(p)||canvasDrag.dragSX)*f),Math.abs(m)<h&&(m=(Math.sign(m)||canvasDrag.dragSY)*h),o&&(Math.abs(p)/a>=Math.abs(m)?m=(Math.sign(m)||canvasDrag.dragSY)*(Math.abs(p)/a):p=(Math.sign(p)||canvasDrag.dragSX)*(Math.abs(m)*a)),p=clamp(p,3*-n,3*n),m=clamp(m,3*-i,3*i);const g=Math.cos(s),v=Math.sin(s),x=e.x+p*g-m*v,b=e.y+p*v+m*g;t.props.x=Math.round((e.x+x)/2-n/2),t.props.y=Math.round((e.y+b)/2-i/2),t.props.boxW=+Math.abs(p).toFixed(1),t.props.boxH=+Math.abs(m).toFixed(1)}}else if("scale"===canvasDrag.mode){const e=toLocal(r,clipBounds(t,evalProps(t,state.time),n,i));t.props.scale=clamp(+(canvasDrag.startScale*(Math.hypot(e.x,e.y)/canvasDrag.startDist)).toFixed(3),.05,12)}else{const a=n/2+(+t.props.x||0),o=i/2+(+t.props.y||0);let s=canvasDrag.startRot+180*(Math.atan2(r.y-o,r.x-a)-canvasDrag.startAng)/Math.PI;e.shiftKey&&(s=15*Math.round(s/15)),t.props.rotation=Math.round(s)}else canvasDidMove=!0}),els.preview.addEventListener("pointerup",endCanvasDrag),els.preview.addEventListener("pointercancel",endCanvasDrag);const _textDirCache=new Map,TEXT_DIR_CACHE_MAX=256;function detectTextDirection(e){const t=e&&String(e).trim()?String(e):" ",n=_textDirCache.get(t);if(n)return n;_textDirProbe||(_textDirProbe=document.createElement("p"),_textDirProbe.style.cssText="position:fixed;left:-9999px;visibility:hidden;white-space:nowrap",document.body.appendChild(_textDirProbe)),_textDirProbe.dir="auto",_textDirProbe.textContent=t;const i="rtl"===getComputedStyle(_textDirProbe).direction?"rtl":"ltr";return _textDirCache.size>=256&&_textDirCache.clear(),_textDirCache.set(t,i),i}function lineDirections(e,t){if("rtl"===e.direction||"ltr"===e.direction){const n=e.direction;return t.map(()=>n)}return t.map(e=>detectTextDirection(e))}function revealWipeRect(e,t,n,i,r,a,o=6){const s=(t+2*o)*r,l=a?e+t/2+o-s:e-t/2-o;ctx2d.rect(l,i-n/2,s,n)}function typewriterX(e,t,n,i){const r=(t-n)/2;return i?e+r:e-r}function runOrigin(e,t,n){return n?e+t/2:e-t/2}const SHAPING_SCRIPT_RE=/[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF\u0900-\u097F\u0980-\u09FF\u0A00-\u0A7F\u0A80-\u0AFF\u0B00-\u0B7F\u0B80-\u0BFF\u0C00-\u0C7F\u0C80-\u0CFF\u0D00-\u0D7F]/;function needsContextualShaping(e){return SHAPING_SCRIPT_RE.test(e)}let _graphemeSeg;const _graphemeCache=new Map,_letterAnimCache=new Map,TEXT_SEG_CACHE_MAX=256;function graphemeSegments(e){const t=String(e??""),n=_graphemeCache.get(t);if(n)return n;let i;return"undefined"!=typeof Intl&&Intl.Segmenter?(_graphemeSeg||(_graphemeSeg=new Intl.Segmenter(void 0,{granularity:"grapheme"})),i=[..._graphemeSeg.segment(t)].map(e=>e.segment)):i=[...t],_graphemeCache.size>=256&&_graphemeCache.clear(),_graphemeCache.set(t,i),i}function letterAnimSegments(e){const t=String(e??""),n=_letterAnimCache.get(t);if(n)return n;let i;if(needsContextualShaping(t)){i=[];const e=/(\s+|[^\s]+)/g;let n;for(;n=e.exec(t);)i.push({text:n[0],animate:!!n[0].trim()})}else i=graphemeSegments(t).map(e=>({text:e,animate:!!e.trim()}));return _letterAnimCache.size>=256&&_letterAnimCache.clear(),_letterAnimCache.set(t,i),i}const backOut=e=>{const t=e-1;return 1+2.70158*t*t*t+1.70158*t*t};function hexToRgba(e,t){const n=parseInt(String(e).replace("#",""),16)||0;return`rgba(${n>>16&255},${n>>8&255},${255&n},${t})`}function hasTextBox(e){return+e.boxW>0&&+e.boxH>0}function textJustifyTarget(e,t){if(+e.boxW>0)return+e.boxW;const n=Math.max(.001,+e.scale||1);return Math.max(t,project.width/n*.85)}function justifyLineBySpaces(e,t,n){const i=String(t).split(/\s+/).filter(Boolean);if(i.length<2)return t;const r=i.join(" ");if(e.measureText(r).width>=n-.5)return r;let a=1,o=2,s=r;for(;o<160&&e.measureText(i.join(" ".repeat(o))).width<n;)o*=2;for(;a<=o;){const t=a+o>>1,r=i.join(" ".repeat(t));e.measureText(r).width<=n?(s=r,a=t+1):o=t-1}return s}function textFontWeight(e){return+e.weight||(e.bold?700:400)}function setTextFont(e,t,n){const i=textFontWeight(t);e.font=`${t.italic?"italic ":""}${i} ${n}px "${t.font||"Segoe UI"}", sans-serif`;try{e.letterSpacing=`${+t.letterSpacing||0}px`}catch{}}function textSourceString(e){let t=String(e.text||"");return e.uppercase&&(t=t.split("\n").map(e=>e.toUpperCase()).join("\n")),t}function wrapTextToWidth(e,t,n){const i=[],r=Math.max(1,n);for(const n of String(t).split("\n")){if(!n){i.push("");continue}const t=n.split(/\s+/).filter(Boolean);if(!t.length){i.push("");continue}let a=t[0];for(let n=1;n<t.length;n++){const o=a+" "+t[n];e.measureText(o).width<=r?a=o:(i.push(a),a=t[n])}i.push(a)}return i.length?i:[""]}const lineHeightOf=e=>clamp(+e.lineHeight||1.2,.6,3);function fitFontSizeToBox(e,t,n,i,r){const a=lineHeightOf(t),o="justify"===t.align,s=textSourceString(t);let l=8,c=Math.max(8,Math.round(r||72)),d=8;for(;l<=c;){const r=l+c>>1;setTextFont(e,t,r);let u=wrapTextToWidth(e,s,n);o&&(u=u.map(t=>justifyLineBySpaces(e,t,n))),Math.max(1,u.length)*r*a<=i+.5?(d=r,l=r+1):c=r-1}return d}function measureTextHalfSize(e){ctx2d.save();const t=e.fontSize||72;setTextFont(ctx2d,e,t);let n=textSourceString(e).split("\n");if("justify"===e.align){const t=textJustifyTarget(e,Math.max(1,...n.map(e=>ctx2d.measureText(e).width)));n=n.map(e=>justifyLineBySpaces(ctx2d,e,t))}const i=Math.max(1,...n.map(e=>ctx2d.measureText(e).width)),r=t*lineHeightOf(e);ctx2d.restore();const a=+e.scale||1;return{hw:(i/2+.25*t)*a,hh:(n.length*r/2+.14*t)*a}}function ensureTextBox(e){if("text"!==e.kind||hasTextBox(e.props))return;const t=measureTextHalfSize(e.props),n=+e.props.scale||1;Math.abs(n-1)>.01&&(e.props.fontSize=Math.round((+e.props.fontSize||72)*n),e.props.scale=1),e.props.boxW=Math.max(40,+(2*t.hw).toFixed(1)),e.props.boxH=Math.max(24,+(2*t.hh).toFixed(1))}function drawText(e,t,n){const i=hasTextBox(t),r=+t.boxW,a=+t.boxH,o=i&&!!t.boxFit,s=textSourceString(t),l=textFontWeight(t);let c=t.fontSize||72;i?o&&(c=fitFontSizeToBox(ctx2d,t,r,a,t.fontSize||72)):ctx2d.scale(t.scale||1,t.scale||1),setTextFont(ctx2d,t,c),ctx2d.textBaseline="middle";let d=i?wrapTextToWidth(ctx2d,s,r):s.split("\n");const u="justify"===t.align;if(u){const e=i?r:textJustifyTarget(t,Math.max(1,...d.map(e=>ctx2d.measureText(e).width)));d=d.map(t=>justifyLineBySpaces(ctx2d,t,e))}const p=c*lineHeightOf(t),m=Math.max(1,d.length),f="top"===t.vAlign||"bottom"===t.vAlign?t.vAlign:"middle";let h;h=i?"top"===f?-a/2+p/2:"bottom"===f?a/2-p/2-(m-1)*p:-(m-1)*p/2:-(m-1)*p/2;const g=TEXT_ANIMS.includes(t.textAnim)?t.textAnim:"none",v=clamp(+t.wordRate||.15,.03,2),x="left"===t.align||"right"===t.align?t.align:"center",b=d.map(e=>ctx2d.measureText(e).width),y=i?r:Math.max(1,...b),T=lineDirections(t,d),k=e=>"left"===x?-y/2+b[e]/2:"right"===x?y/2-b[e]/2:0;i&&(ctx2d.beginPath(),ctx2d.rect(-r/2,-a/2,r,a),ctx2d.clip());const S=(0===t.textShadow?0:+t.textShadow||12)*c/100;if(t.bgOpacity>0){ctx2d.fillStyle=hexToRgba(t.bgColor||"#000",clamp(t.bgOpacity,0,1));const e=.4*c,n=.18*c,i=.28*c;d.forEach((t,r)=>{if(!t.trim())return;const a=b[r],o=h+r*p;ctx2d.beginPath(),ctx2d.roundRect(k(r)-a/2-e,o-p/2-n+.08*p,a+2*e,p+2*n-.16*p,i),ctx2d.fill()})}const w=e=>{if(!t.color2)return t.color||"#fff";const n=ctx2d.createLinearGradient(0,e-.55*c,0,e+.55*c);return n.addColorStop(0,t.color||"#fff"),n.addColorStop(1,t.color2),n},E=(e,n,i,r=1,a=!1)=>{if(r<=0)return;const o=ctx2d.globalAlpha,s=ctx2d.direction;ctx2d.globalAlpha=o*clamp(r,0,1),ctx2d.direction=a?"rtl":"ltr",ctx2d.textAlign="center",t.strokeWidth>0&&(ctx2d.shadowColor="transparent",ctx2d.lineJoin="round",ctx2d.miterLimit=2,ctx2d.lineWidth=t.strokeWidth,ctx2d.strokeStyle=t.strokeColor||"#000",ctx2d.strokeText(e,n,i)),t.glow>0?(ctx2d.shadowColor=t.glowColor||t.color||"#fff",ctx2d.shadowBlur=+t.glow*c/40,ctx2d.fillStyle=w(i),ctx2d.fillText(e,n,i),ctx2d.fillText(e,n,i)):S>0?(ctx2d.shadowColor="rgba(0,0,0,.7)",ctx2d.shadowBlur=S,ctx2d.shadowOffsetY=S/3,ctx2d.fillStyle=w(i),ctx2d.fillText(e,n,i)):(ctx2d.fillStyle=w(i),ctx2d.fillText(e,n,i)),ctx2d.shadowColor="transparent",ctx2d.shadowBlur=0,ctx2d.shadowOffsetY=0,ctx2d.direction=s,ctx2d.globalAlpha=o};if("none"===g)return void d.forEach((e,t)=>E(e,k(t),h+t*p,1,"rtl"===T[t]));if("clip-reveal"===g)return void d.forEach((e,t)=>{if(!e.trim())return;const i=clamp((n-t*v)/.5,0,1);if(i<=0)return;const r=EASE["ease-out"](i),a=b[t],o=k(t),s=h+t*p,l="rtl"===T[t];ctx2d.save(),ctx2d.beginPath(),revealWipeRect(o,a,p,s,r,l),ctx2d.clip(),E(e,o,s,1,l),ctx2d.restore()});if("zoom-in"===g)return void d.forEach((e,t)=>{if(!e.trim())return;const i=clamp((n-t*v)/.45,0,1);if(i<=0)return;const r=1.35-.35*EASE["ease-out"](i),a="rtl"===T[t];ctx2d.save(),ctx2d.translate(k(t),h+t*p),ctx2d.scale(r,r),E(e,0,0,Math.min(1,1.6*i),a),ctx2d.restore()});if("rise-mask"===g)return void d.forEach((e,t)=>{if(!e.trim())return;const i=clamp((n-t*v)/.5,0,1);if(i<=0)return;const r=EASE["ease-out"](i),a=k(t),o=h+t*p,s="rtl"===T[t];ctx2d.save(),ctx2d.beginPath(),ctx2d.rect(a-y/2-24,o-p/2,y+48,p),ctx2d.clip(),E(e,a,o+(1-r)*p,1,s),ctx2d.restore()});if("font-cut"===g){const e=Array.isArray(t.fontCutSet)&&t.fontCutSet.length?t.fontCutSet:FONT_CUT_DEFAULT,i=.6,r=.06;let a=t.font||"Segoe UI";n<i&&(a=e[Math.floor(n/r)%e.length]),ctx2d.font=`${t.italic?"italic ":""}${l} ${c}px "${a}", sans-serif`;const o=d.map(e=>ctx2d.measureText(e).width),s=Math.max(1,...o),u=e=>"left"===x?-s/2+o[e]/2:"right"===x?s/2-o[e]/2:0;return void d.forEach((e,t)=>{e.trim()&&E(e,u(t),h+t*p,1,"rtl"===T[t])})}if("typewriter"===g){let e=Math.floor(n/(v/4));return void d.forEach((t,n)=>{const i="rtl"===T[n],r=graphemeSegments(t),a=r.slice(0,Math.max(0,e)).join("");if(e-=r.length,a){const e=ctx2d.measureText(a).width;E(a,typewriterX(k(n),b[n],e,i),h+n*p,1,i)}})}if("letter-pop"===g||"wave"===g){let e=0;return void d.forEach((t,i)=>{const r=h+i*p,a="rtl"===T[i],o=needsContextualShaping(t),s=letterAnimSegments(t),l=o?v:v/3,d=s.map(e=>ctx2d.measureText(e.text).width),u=d.reduce((e,t)=>e+t,0);let m=runOrigin(k(i),u,a);s.forEach((t,i)=>{const o=d[i],s=a?m-o/2:m+o/2;if(t.animate){if("letter-pop"===g){const i=clamp((n-e*l)/.18,0,1);if(i>0){ctx2d.save(),ctx2d.translate(s,r);const e=Math.max(.001,backOut(i));ctx2d.scale(e,e),E(t.text,0,0,Math.min(1,2.5*i),a),ctx2d.restore()}}else E(t.text,s,r+Math.sin(4*n+.55*e)*c*.12,1,a);e++}m+=a?-o:o})})}const M=ctx2d.measureText(" ").width;let C=0;d.forEach((e,t)=>{const i=h+t*p,r="rtl"===T[t],a=e.split(/\s+/).filter(Boolean),o=a.map(e=>ctx2d.measureText(e).width),s=o.reduce((e,t)=>e+t,0),l=Math.max(0,a.length-1),d=u&&l?b[t]:s+M*l,m=l?(d-s)/l:M;let f=runOrigin(k(t),d,r);a.forEach((e,t)=>{const a=o[t],s=r?f-a/2:f+a/2,l=clamp((n-C*v)/.25,0,1);if("word-pop"===g){if(l>0){ctx2d.save(),ctx2d.translate(s,i);const t=Math.max(.001,backOut(l));ctx2d.scale(t,t),E(e,0,0,Math.min(1,2.5*l),r),ctx2d.restore()}}else"word-slide"===g?l>0&&E(e,s,i+(1-EASE["ease-out"](l))*c*.7,l,r):"bounce"===g?E(e,s,i-Math.abs(Math.sin(3.2*n+.9*C))*c*.18,1,r):"shake"===g?E(e,s+Math.sin(31*n+7.3*C)*c*.035,i+Math.cos(27*n+3.1*C)*c*.035,1,r):E(e,s,i,l>=1?1:.3+.7*l,r);f+=r?-(a+m):a+m,C++})})}async function loadLibraryFonts(){if(runtime.fontsLoading)return runtime.fontsLoading;runtime.fontsLoading=(async()=>{try{const e=await(await fetch("/api/library?dir=fonts")).json();for(const t of e){if(!/\.(ttf|otf|woff2?)$/i.test(t.name))continue;const e=t.name.replace(/\.[^.]+$/,"");if(!runtime.customFonts.includes(e))try{const n=new FontFace(e,`url("${t.src}")`);await n.load(),document.fonts.add(n),runtime.customFonts.push(e)}catch{}}runtime.customFonts.sort()}catch{}})();try{await runtime.fontsLoading}finally{runtime.fontsLoading=null}}let lastTs=null;function loop(e){null==lastTs&&(lastTs=e);const t=Math.min(.1,(e-lastTs)/1e3);lastTs=e,updateScrub(t),updateClipDragScroll(t);const n=projDur();let i=!0;if(!state.rendering){const r=playStopAt(n);runtime.previewWait&&runtime.previewWait.anchor!==state.time&&resetPreviewWait();const a=runtime.previewWait?.t??previewNextTime(t,r),o=previewFrameTime(a,n),s=preparePreviewMedia(a,n);if(!s.length||state.exporting&&!backgroundExportActive)resetPreviewWait(),state.time=a,state.playing&&!runtime.scrub&&state.time>=r&&(state.exporting&&!backgroundExportActive?finishExport(!0):pause()),syncMedia();else{i=!1,silencePreviewMedia();const t=JSON.stringify(project.clips.filter(e=>("video"===e.kind||"audio"===e.kind)&&isTrackEnabled(e.track)&&activeAt(e,o)).map(e=>[e.id,runtime.previewMedia.get(e.id)?.key]));let n=runtime.previewWait;n&&n.signature===t&&n.t===a||(n=runtime.previewWait={t:a,anchor:state.time,since:e,signature:t}),!n.reported&&e-n.since>=200&&(els.btnPlay.title=uiText("Buffering…"),els.btnPlay.textContent="⏳");const r=s.find(e=>e.error);if(r||e-n.since>=15e3){const{c:e,error:t}=r||s[0];n.reported||(pause(),n.reported=!0,runtime.previewWait=n,toast(uiText("Video preview: {name}: {error}",{name:getMedia(e.mediaId)?.name||e.mediaId||e.id,error:t||uiText("loading timed out")})))}}i&&drawFrame(previewFrameTime(state.time,n))}if(state.playing&&!runtime.scrub&&!1!==runtime.followPlayhead){const e=state.time*state.pps,t=els.timelineScroll;(e<t.scrollLeft||e>t.scrollLeft+t.clientWidth-40)&&scrollTimeline(Math.max(0,e-60))}state.dirtyTimeline&&rebuildClips(),updateClipThumbs(),updateMissingMediaNotice(),els.playhead.style.left=state.time*state.pps+"px",drawRuler(),updateSafeOverlay(),updateKfGraphs(),updateMeterUI(t),els.tcCurrent.textContent=fmt(state.time),els.tcTotal.textContent=fmt(n),!state.exporting||state.rendering||backgroundExportActive||realtimeStarting||realtimeFinishing||updateExportProgress(n?state.time/n*100:0,uiText("Quick export in progress. Keep this page open and in the foreground.")),requestAnimationFrame(loop)}let exportOpening=!1,realtimeStarting=!1,realtimeFinishing=!1,exportPreviewUrl="",exportDetails=null,exportResult=null,exportSaveController=null;function showExportDialog(){rememberExportDialog({open:!0}),els.exportOverlay.classList.remove("hidden"),$("btnCloseExport").focus()}function hideExportDialog(){rememberExportDialog({open:!1}),dismissExportCancellation(),exportSaveController?.abort(),$("exportVideo").pause(),els.exportOverlay.classList.add("hidden"),$("btnExport").focus()}function cancelExport(){if(backgroundExportActive)renderCancelled=!0,cancelBackgroundExport();else if(state.rendering&&!realtimeStarting)renderCancelled=!0,optimizedAbort?.abort(),fastExportAbort?.abort();else if(state.exporting)finishExport(!1);else{const e=readRememberedBackgroundExport();if(!e)return;backgroundExportId=e,backgroundExportActive=state.exporting=renderCancelled=!0,cancelBackgroundExport().then(e=>endBackgroundExportUi(e))}}function dismissExportCancellation(){$("cancelExportDialog").open&&$("cancelExportDialog").close()}function exportNeedsCancellation(){const e=readExportDialog();return!("complete"===e.phase||"error"===e.phase&&e.canRecord)&&(state.exporting||realtimeStarting||realtimeFinishing||!!readRememberedBackgroundExport())}function closeExportDialog(){if(exportNeedsCancellation())return $("cancelExportDialog").open||$("cancelExportDialog").showModal(),void $("btnContinueExport").focus();forgetBackgroundExport(),hideExportDialog()}function confirmCancelExport(){exportNeedsCancellation()?cancelExport():forgetBackgroundExport(),hideExportDialog()}function updateExportProgress(e,t,n="working"){const i="complete"===n?100:Math.max(0,Math.min(99,Number(e)||0));rememberExportDialog({phase:n,percent:i,message:t}),els.exportOverlay.dataset.status=n,els.exportProgress.style.width=i.toFixed(1)+"%",$("exportPercent").textContent=Math.floor(i)+"%",$("exportProgressTrack").setAttribute("aria-valuenow",Math.floor(i)),els.exportNote.title="",els.exportNote.textContent!==t&&(els.exportNote.textContent=t)}function syncExportTrackWarning(){const e=$("exportTrackWarn"),t=TRACKS.filter(e=>!isTrackEnabled(e.id)&&project.clips.some(t=>t.track===e.id)).map(e=>e.id);if(t.length&&e){const n=t.join(", ");e.textContent=1===t.length?uiText("Track {tracks} is disabled and will be omitted from the export.",{tracks:n}):uiText("Tracks {tracks} are disabled and will be omitted from the export.",{tracks:n}),e.classList.remove("hidden")}else e&&(e.textContent="",e.classList.add("hidden"))}function resetExportDialog(e=null){$("exportChecks")&&($("exportChecks").hidden=!0,$("exportChecks").open=!1),exportSaveController?.abort(),exportSaveController=null,exportResult=null,$("btnSaveExport").disabled=!1,$("exportSaveLabel").textContent=uiText("Save"),setExportSaveNote("");const t=$("exportVideo");t.pause(),t.removeAttribute("src"),t.removeAttribute("poster"),t.load(),t.controls=!1,exportPreviewUrl&&URL.revokeObjectURL(exportPreviewUrl),exportPreviewUrl="",exportDetails=e||{name:project.name||uiText("Untitled Project"),duration:Math.max(1/project.fps,Math.round(projDur()*project.fps)/project.fps),width:project.width,height:project.height,fps:project.fps,clips:project.clips.filter(e=>isVisualClip(e)&&isTrackEnabled(e.track)).map(e=>({start:e.start,duration:e.duration,kind:e.kind})),renderStartedAt:0};const{duration:n,width:i,height:r}=exportDetails;e?.poster&&(t.poster=e.poster),els.exportTitle.textContent=exportDetails.name,els.exportTitle.title=exportDetails.name,$("exportDuration").textContent=uiText("Video duration {seconds}s",{seconds:n.toFixed(1)}),$("exportTime").textContent=`${String(Math.floor(n/60)).padStart(2,"0")}:${(n%60).toFixed(1).padStart(4,"0")}`;const a=i/r;$("exportPreview").style.setProperty("--export-aspect",`${i} / ${r}`),$("exportPreview").style.setProperty("--export-preview-width",`min(${Math.min(476,398*a)}px, ${Math.min(58,58*a)}dvh)`),$("btnExportPreview").hidden=!1,$("btnExportPreview").disabled=!0,$("exportTime").hidden=!1;for(const e of["btnExportFallback","btnRetryExport","btnCancelExport","btnSaveExport"])$(e).hidden=!0;syncExportTrackWarning(),rememberBackgroundExportDetails(),updateExportProgress(0,uiText("Preparing video…")),showExportDialog()}let coverCaptureQueue=Promise.resolve(),coverStillActive=!1;function captureTimelineStill(e,t=720,n=()=>!0){const i=coverCaptureQueue.then(()=>n()?captureTimelineStillNow(e,t):null);return coverCaptureQueue=i.catch(()=>{}),i}async function captureTimelineStillNow(e,t=720){const n=state.time,i=state.rendering,r=state.exporting;state.rendering=!0,state.exporting=!0,coverStillActive=!0;try{await loadLibraryFonts(),await document.fonts.ready,await seekVideosTo(e),await Promise.all(visibleClipsAt(e).filter(e=>"image"===e.kind||"svg"===e.kind).map(async e=>{const t=getMedia(e.mediaId);if(t)if("image"!==e.kind||runtime.mediaAux.get(t.id)?.img)"svg"!==e.kind||runtime.mediaAux.get(t.id)?.svgText||await loadSvgMedia(t);else{const e=await loadImage(t.src);runtime.mediaAux.set(t.id,{...runtime.mediaAux.get(t.id),img:e})}})),await prepareFrameAssets(e),drawFrame(e);const n=document.createElement("canvas");return n.width=Math.min(t,els.preview.width),n.height=Math.round(n.width*els.preview.height/els.preview.width),n.getContext("2d").drawImage(els.preview,0,0,n.width,n.height),n.toDataURL("image/jpeg",.9)}finally{coverStillActive=!1,state.time=n,state.rendering=i,state.exporting=r&&!renderCancelled,seekMediaWhilePaused()}}async function coverPoster(e,t=720,n=()=>!0){if("image"===e?.type){const n=await loadImage(e.src),i=document.createElement("canvas");i.width=Math.min(t,project.width),i.height=Math.max(1,Math.round(i.width*project.height/project.width));const r=i.getContext("2d"),a=Math.min(i.width/n.width,i.height/n.height);return r.fillStyle=project.background||"#000",r.fillRect(0,0,i.width,i.height),r.drawImage(n,(i.width-n.width*a)/2,(i.height-n.height*a)/2,n.width*a,n.height*a),i.toDataURL("image/jpeg",.9)}const i=Math.max(0,(Math.max(1,Math.round(projDur()*project.fps))-1)/project.fps);return captureTimelineStill(clamp(e?.time||0,0,i),t,n)}async function captureExportPoster(){try{$("exportVideo").poster=await coverPoster(project.cover),rememberBackgroundExportDetails()}catch{}}let coverDraft=null,coverSession=0,coverRequest=0,coverFrameTime=0,coverButtonKey="",coverDraftReady=!1,coverUploading=!1;function coverThumbnailKey(){const e=project.media.map(({id:e,kind:t,src:n,duration:i,width:r,height:a})=>({id:e,kind:t,src:n,duration:i,width:r,height:a}));return JSON.stringify([activeProjectId,project.cover,project.clips,e,project.tracks,project.width,project.height,project.fps,project.background,project.disabledTracks,[...mediaSourceVersions]])}async function coverThumbnailStorage(e){return withClipThumbStorage(async()=>{const t=clamp(project.cover?.time||0,0,Math.max(0,(Math.round(projDur()*project.fps)-1)/project.fps)),n="image"===project.cover?.type?[project.cover.src]:visibleClipsAt(t).map(e=>getMedia(e.mediaId)?.src).filter(Boolean),i=await Promise.all([...new Set(n)].map(clipThumbSourceVersion));if(i.some(e=>!e))return null;const r=(new TextEncoder).encode(JSON.stringify([e,i])),a=[...new Uint8Array(await crypto.subtle.digest("SHA-256",r))].map(e=>e.toString(16).padStart(2,"0")).join("");return{cache:await caches.open("fablecut-cover-thumbs-v1"),key:location.origin+"/__fablecut_cover__/"+a}})}function storeCoverThumbnail(e,t){e&&validStoredClipThumb(t)&&withClipThumbStorage(async()=>{await e.cache.put(e.key,new Response(t));const n=await e.cache.keys();for(const t of n.slice(0,Math.max(0,n.length-32)))await e.cache.delete(t)})}let coverButtonPending=null;function refreshCoverButton(){if($("btnDeleteCover").hidden=!project.cover,!getSetting("showCover"))return void(coverButtonPending=null);const e=coverThumbnailKey(),t=$("coverThumbnail");if(e===coverButtonKey)return coverButtonPending=null,void(t.hidden=!project.cover);if(e===coverButtonPending?.key)return;if(t.hidden=!0,!project.cover)return coverButtonPending=null,void(coverButtonKey=e);const n=coverButtonPending={key:e},i=()=>coverButtonPending===n&&e===coverThumbnailKey()&&getSetting("showCover");(async()=>{try{const n=await coverThumbnailStorage(e);if(!i())return;const r=n&&await withClipThumbStorage(async()=>{const e=await n.cache.match(n.key),t=e&&await e.text();if(validStoredClipThumb(t))try{return await loadImage(t),t}catch{}return e&&await n.cache.delete(n.key),null});if(!i())return;let a=r;if(!a){for(;i()&&(state.playing||state.rendering||state.exporting||state.gesture||exportOpening||document.hidden||$("coverDialog").open);)await new Promise(e=>setTimeout(e,100));if(!i())return;a=await coverPoster(project.cover,120,i)}if(!a||!i())return;t.src=a,t.hidden=!1,coverButtonKey=e,r||storeCoverThumbnail(n,a)}catch{}finally{coverButtonPending===n&&(coverButtonPending=null,getSetting("showCover")&&project.cover&&e!==coverThumbnailKey()&&refreshCoverButton())}})()}function updateCoverControls(){const e="image"!==coverDraft?.type;$("btnCoverFrame").setAttribute("aria-pressed",String(e)),$("btnCoverImage").setAttribute("aria-pressed",String(!e)),$("btnCoverFrame").disabled=projDur()<=0||coverUploading,$("btnCoverImage").disabled=coverUploading,$("btnSetCover").disabled=!coverDraftReady||coverUploading,$("coverTimeline").classList.toggle("inactive",!e||coverUploading),$("coverTimeline").setAttribute("aria-disabled",String(!e||coverUploading||projDur()<=0)),$("coverTimeline").setAttribute("aria-valuemax",String(lastCoverFrameTime())),$("coverTimeline").setAttribute("aria-valuenow",String(coverFrameTime)),drawCoverPlayhead()}async function previewCoverDraft(){const e=coverSession,t=++coverRequest;coverDraftReady=!1,updateCoverControls(),$("coverStatus").textContent=uiText("Loading cover…");try{const n=await coverPoster(coverDraft,720,()=>e===coverSession&&t===coverRequest&&$("coverDialog").open);if(e!==coverSession||t!==coverRequest||!$("coverDialog").open)return;$("coverPreview").src=n,$("coverPreview").hidden=!1,$("coverStatus").textContent="",coverDraftReady=!0}catch{if(e!==coverSession||t!==coverRequest)return;$("coverPreview").hidden=!0,$("coverStatus").textContent=uiText("Cover could not be loaded")}updateCoverControls()}function lastCoverFrameTime(){return Math.max(0,(Math.max(1,Math.round(projDur()*project.fps))-1)/project.fps)}function selectCoverFrame(e){coverFrameTime=clamp(Math.round(e*project.fps)/project.fps,0,lastCoverFrameTime()),coverDraft={type:"frame",time:coverFrameTime},previewCoverDraft()}function drawCoverPlayhead(){if(!$("coverDialog").open)return;const e=$("coverFrames").clientWidth,t=e/Math.max(projDur(),.001),n=coverFrameTime;$("coverPlayhead").style.left=n*t+"px",$("coverRuler").style.width=`${e}px`,$("coverRuler").style.height="26px",drawRulerMainThread(e,26,window.devicePixelRatio||1,$("coverRuler"),0,t,n,!1)}function renderCoverTimeline(){if(!$("coverDialog").open)return;const e=$("coverFrames"),t=e.clientWidth,n=t/Math.max(projDur(),.001),i=[...new Set(project.clips.filter(e=>"video"===e.kind||"image"===e.kind).map(e=>e.track))].filter(isTrackEnabled).sort((e,t)=>TRACKS.findIndex(t=>t.id===e)-TRACKS.findIndex(e=>e.id===t));e.replaceChildren();for(const r of i){const i=document.createElement("div");i.className="track",i.style.height="64px";for(const e of project.clips.filter(e=>e.track===r&&("video"===e.kind||"image"===e.kind))){const r=document.createElement("div");r.className=`clip c-${e.kind}`,r.style.left=e.start*n+"px",r.style.width=e.duration*n+"px";const a=document.createElement("div");if(a.className="clip-body","video"===e.kind)a.innerHTML=clipThumbStrip(e,64,n,{scrollLeft:0,clientWidth:t});else{const t=document.createElement("img");t.src=mediaLoadSrc(getMedia(e.mediaId)||{src:""}),t.alt="",a.append(t)}r.append(a),i.append(r)}e.append(i)}drawCoverPlayhead(),loadClipThumbs()}async function openCoverDialog(){!coverStillActive||exportOpening||backgroundExportActive||await coverCaptureQueue,state.rendering||state.exporting||state.gesture||$("coverDialog").open||(pause(),++coverSession,coverDraft=project.cover?structuredClone(project.cover):{type:"frame",time:state.time},coverFrameTime=clamp(coverDraft.time||0,0,lastCoverFrameTime()),"frame"===coverDraft.type&&(coverDraft.time=coverFrameTime),$("coverPreview").hidden=!0,$("coverDialog").showModal(),renderCoverTimeline(),$("btnCancelCover").focus(),await previewCoverDraft())}document.addEventListener("pointerdown",e=>{const t=$("coverControl"),n=document.activeElement;!t.contains(e.target)&&t.contains(n)&&n.blur()},!0),new ResizeObserver(()=>{$("coverDialog").open&&renderCoverTimeline()}).observe($("coverFrames")),$("coverTimeline").addEventListener("pointerdown",e=>{if(0!==e.button||coverUploading||"image"===coverDraft?.type||projDur()<=0)return;e.preventDefault();const t=$("coverTimeline"),n=e=>{const t=$("coverFrames").getBoundingClientRect();selectCoverFrame(clamp((e.clientX-t.left)/t.width*projDur(),0,lastCoverFrameTime()))};n(e);try{t.setPointerCapture(e.pointerId)}catch{}const i=t=>{t.pointerId===e.pointerId&&n(t)},r=n=>{n.pointerId===e.pointerId&&(t.removeEventListener("pointermove",i),t.removeEventListener("lostpointercapture",r),t.removeEventListener("pointerup",r),t.removeEventListener("pointercancel",r),t.hasPointerCapture(e.pointerId)&&t.releasePointerCapture(e.pointerId))};t.addEventListener("pointermove",i),t.addEventListener("pointerup",r),t.addEventListener("pointercancel",r),t.addEventListener("lostpointercapture",r)}),$("btnCover").addEventListener("click",openCoverDialog);for(const e of["btnCloseCover","btnCancelCover"])$(e).addEventListener("click",()=>$("coverDialog").close());function clearCover(){project.cover&&(pushUndo(),project.cover=null,scheduleSave())}function failExportDialog(e,t=!0,n=parseFloat(els.exportProgress.style.width)){dismissExportCancellation();const{engine:i}=readExportDialog(),r="realtime"===i||"local"===i;let a=uiText("Export interrupted. Try quick export to export again.");t?"realtime"===i?a=uiText("Background recording failed. You can record in this page while it stays open."):"local"===i&&(a=uiText("Recording failed. Keep this page open to try again.")):a=uiText("Export failed: ")+e,updateExportProgress(n,a,"error"),els.exportNote.title=e||"",$("btnExportFallback").hidden=!t,$("btnExportFallback").querySelector("span").textContent=uiText(r?"Record in this page":"Quick export"),$("btnRetryExport").hidden=t,$("btnCancelExport").hidden=!0,rememberExportDialog({error:e,canRecord:t})}function setExportSaveNote(e,t=""){const n=$("exportSaveNote");n.textContent=e,n.hidden=!e,n.title=t,n.classList.toggle("error",!!t)}async function saveCompletedExport(){if(!exportResult||exportSaveController)return;const e=exportResult,t=new AbortController;exportSaveController=t,$("btnSaveExport").disabled=!0,$("exportSaveLabel").textContent=uiText("Saving…"),setExportSaveNote("");try{await saveExportInBrowser(e,t.signal)}catch(n){e!==exportResult||t.signal.aborted||setExportSaveNote(uiText("Unable to save. Click Save to try again."),n.message)}finally{exportSaveController===t&&(exportSaveController=null,$("btnSaveExport").disabled=!1,$("exportSaveLabel").textContent=uiText("Save"))}}async function saveExportInBrowser(e,t){t.throwIfAborted();const n=e.filename||decodeURIComponent(e.src.split("/").pop()),i=document.createElement("a");i.href=e.src,i.download=n,i.click(),e!==exportResult||t.aborted||setExportSaveNote(uiText("Check your browser downloads to confirm the file was saved."))}function completeExportDialog(e,t="",n){dismissExportCancellation();const i=readExportDialog();exportResult={src:e,filename:t};const r=$("exportChecks"),a=n||i.outputs||[];if(r){r.hidden=!a.length;const e=$("exportCheckResults");e.replaceChildren();for(const t of a){const n=document.createElement("p");if(t.src){const e=document.createElement("a");e.href=t.src,e.download=t.name||"",e.textContent=t.name||decodeURIComponent(t.src.split("/").pop()),n.append(e,":")}n.append(document.createTextNode(t.error||t.report?.line||uiText("Checks unavailable"))),e.append(n)}}updateExportProgress(100,uiText("Video complete. Your file is ready."),"complete"),$("btnCancelExport").hidden=!0,$("exportVideo").src=e,$("btnExportPreview").disabled=!1,$("btnSaveExport").hidden=!1,rememberExportDialog({src:e,filename:t,outputs:a}),new URLSearchParams(location.search).has("exportJob")||i.savePrompted&&i.src===e||(rememberExportDialog({savePrompted:!0}),saveCompletedExport())}function updateBackgroundExportProgress(e){const t=e.totalFrames?(e.frames||0)/e.totalFrames*100:0;let n=uiText("Preparing video…");if("queued"===e.state)n=uiText("Queued for export…");else if("finalizing"===e.state)n=uiText("Finalizing video…");else if("rendering"===e.state){const i=Math.max(0,((e.frames||1)-1)/exportDetails.fps),r=exportDetails.clips.filter(e=>"video"===e.kind),a=(r.length?r:exportDetails.clips).slice().sort((e,t)=>e.start-t.start),o=a.findIndex(e=>e.start<=i&&e.start+e.duration>i);if(n=o<0?uiText("Rendering video…"):uiText("Rendering clip {current}/{total}",{current:o+1,total:a.length}),exportDetails.renderStartedAt||(exportDetails.renderStartedAt=Date.now()),t>0&&t<99&&Date.now()-exportDetails.renderStartedAt>2e3){const e=Math.ceil((Date.now()-exportDetails.renderStartedAt)/1e3*(100-t)/t);n+=uiText(" · About {seconds}s remaining",{seconds:e})}e.wait&&(n=uiText("Preparing the next part of your video. Export will continue automatically.")),"realtime"===e.engine&&(n=uiText("Quick export in progress. You can switch chats."))}updateExportProgress("finalizing"===e.state?99:t,n)}$("coverDialog").addEventListener("keydown",e=>e.stopPropagation()),$("coverDialog").addEventListener("close",()=>{$("coverDialog").open||(coverSession++,coverRequest++,coverUploading=!1,$("coverFrames").replaceChildren(),state.dirtyTimeline=!0,$("coverFile").value="",refreshCoverButton(),$("btnCover").focus(),runtime.pendingSync&&syncFromServer())}),$("coverTimeline").addEventListener("keydown",e=>{if(coverUploading||"image"===coverDraft?.type||projDur()<=0)return;const t={ArrowLeft:-1,ArrowDown:-1,ArrowRight:1,ArrowUp:1};(e.key in t||"Home"===e.key||"End"===e.key)&&(e.preventDefault(),selectCoverFrame("Home"===e.key?0:"End"===e.key?lastCoverFrameTime():coverFrameTime+t[e.key]/project.fps))}),$("btnCoverFrame").addEventListener("click",()=>selectCoverFrame(coverFrameTime)),$("btnCoverImage").addEventListener("click",()=>$("coverFile").click()),$("coverFile").addEventListener("change",async()=>{const e=$("coverFile").files[0];if($("coverFile").value="",!e)return;if(!/\.(png|jpe?g|webp|gif)$/i.test(e.name))return void toast(uiText("Choose a PNG, JPEG, WebP or GIF image"));const t=coverSession;let n;coverUploading=!0,coverRequest++,coverDraftReady=!1,updateCoverControls(),$("coverStatus").textContent=uiText("Loading cover…");try{n=URL.createObjectURL(e),await loadImage(n);const i=await fetch(projectApi("/api/upload?name="+encodeURIComponent("cover-"+uid()+"-"+e.name)),{method:"POST",body:e});if(!i.ok)throw new Error("upload");const r=await i.json();if(t!==coverSession||!$("coverDialog").open)return;coverDraft={type:"image",src:r.src},coverUploading=!1,await previewCoverDraft()}catch{if(t!==coverSession)return;$("coverStatus").textContent=uiText("Cover image upload failed")}finally{n&&URL.revokeObjectURL(n),t===coverSession&&(coverUploading=!1,updateCoverControls())}}),$("btnSetCover").addEventListener("click",()=>{if(!coverDraftReady||coverUploading)return;pushUndo(),project.cover=structuredClone(coverDraft);const e=$("coverPreview").src,t=coverThumbnailKey();$("coverThumbnail").src=e,$("coverThumbnail").hidden=!1,coverButtonKey=t,Promise.all([coverThumbnailStorage(t),loadImage(e)]).then(([e,t])=>{const n=document.createElement("canvas");n.width=Math.min(120,t.width),n.height=Math.max(1,Math.round(n.width*t.height/t.width)),n.getContext("2d").drawImage(t,0,0,n.width,n.height),storeCoverThumbnail(e,n.toDataURL("image/jpeg",.9))}).catch(()=>{}),scheduleSave(),$("coverDialog").close()}),$("btnDeleteCover").addEventListener("click",e=>{e.stopPropagation(),clearCover(),$("btnCover").focus()}),$("btnResetCover").addEventListener("click",()=>{clearCover(),$("coverDialog").close()});let backgroundExportId=null,backgroundExportActive=!1,backgroundExportCancellation=null;function cancelBackgroundExport(){return backgroundExportId?(backgroundExportCancellation||(backgroundExportCancellation=fetch(projectApi("/api/export/cancel?id="+backgroundExportId),{method:"POST"}).then(e=>e.ok||404===e.status).catch(()=>!1)),backgroundExportCancellation):Promise.resolve(!1)}function backgroundExportStorageKey(){return"fablecut.backgroundExport:"+activeProjectId}function readExportDialog(){try{return JSON.parse(sessionStorage.getItem(backgroundExportStorageKey()+":dialog"))||{}}catch{return{}}}function rememberExportDialog(e){try{sessionStorage.setItem(backgroundExportStorageKey()+":dialog",JSON.stringify({...readExportDialog(),...e}))}catch{}}function rememberBackgroundExportDetails(){if(exportDetails)try{sessionStorage.setItem(backgroundExportStorageKey()+":details",JSON.stringify({...exportDetails,poster:$("exportVideo").poster,renderStartedAt:0}))}catch{}}function rememberBackgroundExport(e){try{const t=backgroundExportStorageKey();sessionStorage.setItem(t,e),rememberBackgroundExportDetails()}catch{}}function forgetBackgroundExport(){try{sessionStorage.removeItem(backgroundExportStorageKey()),sessionStorage.removeItem(backgroundExportStorageKey()+":details"),sessionStorage.removeItem(backgroundExportStorageKey()+":dialog")}catch{}}function readBackgroundExportDetails(){try{const e=JSON.parse(sessionStorage.getItem(backgroundExportStorageKey()+":details"));return e&&"string"==typeof e.name&&Array.isArray(e.clips)&&[e.duration,e.fps,e.width,e.height].every(e=>Number.isFinite(e)&&e>0)?e:null}catch{return null}}function readRememberedBackgroundExport(){try{const e=sessionStorage.getItem(backgroundExportStorageKey());return/^[a-f0-9]{32}$/.test(e||"")?e:""}catch{return""}}function beginBackgroundExportUi(e=null){backgroundExportActive=!0,state.exporting=!0,renderCancelled=!1,resetExportDialog(e)}function endBackgroundExportUi(e=!0){backgroundExportId=null,backgroundExportActive=!1,backgroundExportCancellation=null,state.exporting=!1,e&&forgetBackgroundExport(),renderCancelled&&hideExportDialog(),runtime.pendingSync&&syncFromServer()}async function followBackgroundExport(e){backgroundExportId=e,rememberBackgroundExport(e);let t=!1;try{for(;!renderCancelled;){const n=await fetch(projectApi("/api/export/status?id="+e),{cache:"no-store"}),i=await n.json();if(renderCancelled)break;if(!n.ok)throw new Error(i.error||"Export status unavailable");if(t=["complete","error","cancelled"].includes(i.state),"error"===i.state){failExportDialog(i.error||"",!0,i.totalFrames?(i.frames||0)/i.totalFrames*100:void 0);break}if("cancelled"===i.state){renderCancelled=!0;break}if("complete"===i.state){if(!i.src)throw new Error(uiText("The exported file is unavailable. Please retry."));completeExportDialog(i.src,"",i.outputs);break}updateBackgroundExportProgress(i),await new Promise(e=>setTimeout(e,500))}}catch(e){renderCancelled||failExportDialog(e.message,t)}finally{renderCancelled&&(t=await cancelBackgroundExport()),endBackgroundExportUi(renderCancelled&&t)}}async function backgroundExport(e,t={}){if(state.exporting)return;pause();const n="realtime"===e?readBackgroundExportDetails():null;forgetBackgroundExport(),beginBackgroundExportUi(n),rememberExportDialog({engine:e});let i,r="";try{if("realtime"!==e&&await captureExportPoster(),runtime.savePromise&&await runtime.savePromise,runtime.saveTimer&&(clearTimeout(runtime.saveTimer),runtime.saveTimer=null,await saveProjectNow()),renderCancelled)return;"realtime"===e&&(i=Array.from(crypto.getRandomValues(new Uint8Array(16)),e=>e.toString(16).padStart(2,"0")).join(""),rememberBackgroundExport(i),rememberExportDialog({submittedAt:Date.now()}));const n=await fetch(projectApi("/api/export/start"),{method:"POST",keepalive:!!i,headers:{"Content-Type":"application/json"},body:JSON.stringify({engine:e,revision:project.revision,requestId:i,fallback:!0===t.fallback,...["optimized","fast"].includes(e)&&!project.delivery&&"delivery"!==getSetting("exportProfile")?{profile:getSetting("exportProfile")}:{}})}),a=await n.json();if(!n.ok)throw i&&(rememberBackgroundExport(""),i=null),new Error(a.error||"Export start failed");r=a.id}catch(e){renderCancelled?i&&(r=i):failExportDialog(e.message,!i)}finally{r||endBackgroundExportUi(renderCancelled)}r&&await followBackgroundExport(r)}async function resumeBackgroundExport(e=!1){const t=new URLSearchParams(location.search);if(state.exporting||t.has("exportJob")||t.has("cliExport"))return!1;const n=readRememberedBackgroundExport(),i=readExportDialog();if(!(n||e&&i.open))return!1;renderCancelled=!1;const r=readBackgroundExportDetails();if(!n)return resetExportDialog(r),"complete"===i.phase&&i.src&&!i.src.startsWith("blob:")?completeExportDialog(i.src,i.filename,i.outputs):failExportDialog(i.error||uiText("The exported file is unavailable. Please retry."),!0,i.percent),!0;let a;try{const e=()=>fetch(projectApi("/api/export/status?id="+n),{cache:"no-store"});let t=await e();for(let n=0;n<12&&!renderCancelled&&404===t.status&&Date.now()-i.submittedAt<3e3;n++)await new Promise(e=>setTimeout(e,250)),t=await e();if(a=await t.json(),renderCancelled)return!0;if(404===t.status)return forgetBackgroundExport(),resetExportDialog(r),"realtime"===i.engine&&rememberExportDialog({engine:i.engine}),failExportDialog(uiText("The exported file is unavailable. Please retry.")),!0;if(!t.ok)throw new Error(a.error||uiText("Unable to check export progress. Please retry."))}catch(e){return renderCancelled||(resetExportDialog(r),failExportDialog(uiText("Unable to check export progress. Please retry."),!1)),!0}return"cancelled"===a.state?(forgetBackgroundExport(),!1):"complete"===a.state?(pause(),resetExportDialog(r),r||await captureExportPoster(),renderCancelled?(forgetBackgroundExport(),!0):(a.src?completeExportDialog(a.src,"",a.outputs):failExportDialog(uiText("The exported file is unavailable. Please retry.")),!0)):"error"===a.state?(pause(),resetExportDialog(r),r||await captureExportPoster(),failExportDialog(a.error||"",!0,a.totalFrames?(a.frames||0)/a.totalFrames*100:void 0),!0):(beginBackgroundExportUi(r),pause(),r||await captureExportPoster(),await followBackgroundExport(n),!0)}async function restoreExportDialog(){const e=new URLSearchParams(location.search);if(!(!state.connected||state.exporting||exportOpening||e.has("exportJob")||e.has("cliExport"))&&readExportDialog().open){exportOpening=!0;try{await resumeBackgroundExport(!0)}finally{exportOpening=!1}}}async function openExportButton(){if(!coverStillActive||exportOpening||backgroundExportActive||await coverCaptureQueue,state.exporting)showExportDialog();else if(!(exportOpening||realtimeStarting||realtimeFinishing)){exportOpening=!0;try{if(await resumeBackgroundExport())return;if(!project.clips.length)return void alert(uiText("Timeline is empty — add some clips first."));const e=getSetting("exportEngine");await backgroundExport(["optimized","fast","webcodecs"].includes(e)?e:"webcodecs")}finally{exportOpening=!1}}}let renderCancelled=!1,fastExportAbort=null;async function prepareVideoFrame(e,t,n){const i=i=>new Error(`Video "${getMedia(e.mediaId)?.name||e.mediaId}" (clip ${e.id}, source ${n.toFixed(4)}s): ${i} (readyState=${t?.readyState}, size=${t?.videoWidth}x${t?.videoHeight}, currentTime=${t?.currentTime})`);if(!t)throw i("missing media");t.paused||t.pause();const r=(e,n,r,a=()=>{})=>new Promise((o,s)=>{const l=["loadedmetadata","loadeddata","canplay","seeked","error"],c=e=>{clearTimeout(u);for(const e of l)t.removeEventListener(e,d);e?s(e):o()},d=()=>t.error?c(i(`decode failed: ${t.error.message||"media error "+t.error.code}`)):!(t.readyState>=1)||t.videoWidth&&t.videoHeight?void(e()&&c()):c(i("no decodable video track; use a supported codec or transcode the source to H.264")),u=setTimeout(()=>c(i(`${r} timed out after ${n/1e3}s`)),n);for(const e of l)t.addEventListener(e,d);try{a(),d()}catch(e){c(i(e.message))}});await r(()=>t.readyState>=1,15e3,"metadata load"),await r(()=>!t.seeking&&t.readyState>=2&&Math.abs(t.currentTime-n)<1e-4,1e4,"frame seek",()=>{Math.abs(t.currentTime-n)>=1e-4&&(t.currentTime=n)})}function seekVideosTo(e){const t=project.clips.filter(t=>"video"===t.kind&&isTrackEnabled(t.track)&&activeAt(t,e));for(const n of t)stealExportVideo(n,e);return releaseFinishedExportVideos(e),Promise.all(t.map(async t=>{await decodeExportVideo(t,e)||await prepareVideoFrame(t,acquireExportVideo(t,e),mediaTimeAt(t,e))}))}function encodeWAV(e){const t=e.numberOfChannels,n=e.length,i=e.sampleRate,r=44+n*t*2,a=new ArrayBuffer(r),o=new DataView(a),s=(e,t)=>{for(let n=0;n<t.length;n++)o.setUint8(e+n,t.charCodeAt(n))};s(0,"RIFF"),o.setUint32(4,r-8,!0),s(8,"WAVE"),s(12,"fmt "),o.setUint32(16,16,!0),o.setUint16(20,1,!0),o.setUint16(22,t,!0),o.setUint32(24,i,!0),o.setUint32(28,i*t*2,!0),o.setUint16(32,2*t,!0),o.setUint16(34,16,!0),s(36,"data"),o.setUint32(40,n*t*2,!0);const l=[];for(let n=0;n<t;n++)l.push(e.getChannelData(n));let c=44;for(let e=0;e<n;e++)for(let n=0;n<t;n++){const t=clamp(l[n][e],-1,1);o.setInt16(c,t<0?32768*t:32767*t,!0),c+=2}return new Blob([a],{type:"audio/wav"})}function wsolaStretchChannels(e,t,n,i){const r=e.length,a=Array.from({length:r},()=>new Float32Array(n));if(!r||n<=0)return a;const o=e[0].length;if(!o)return a;let s=Math.round(.03*t);s%2!=0&&s++;const l=s>>1,c=Math.round(.012*t);if(n<=l){const s=Math.round(i(0)*t);for(let t=0;t<r;t++){const i=e[t],r=a[t];for(let e=0;e<n;e++){const t=s+e;t>=0&&t<o&&(r[e]=i[t])}}return a}const d=new Float32Array(s);for(let e=0;e<s;e++)d[e]=.5*(1-Math.cos(2*Math.PI*e/s));let u=i(0),p=u;const m=Math.min(64,Math.max(8,Math.ceil(n/l)));for(let e=1;e<=m;e++){const r=i(e/m*(n/t));r<u&&(u=r),r>p&&(p=r)}const f=Math.max(0,Math.floor(u*t)),h=Math.min(o,Math.ceil(p*t)),g=Math.max(0,f-c-s),v=Math.min(o,h+c+2*s),x=Math.max(0,v-g),b=new Float32Array(x);if(1===r)b.set(e[0].subarray(g,v));else{const t=1/r;for(let n=0;n<r;n++){const i=e[n].subarray(g,v);for(let e=0;e<x;e++)b[e]+=i[e]*t}}const y=Math.round(i(0)*t);let T=y;for(let t=0;t<r;t++){const i=e[t],r=a[t];for(let e=0;e<l&&e<n;e++){const t=y+e;t>=0&&t<o&&(r[e]=i[t])}for(let e=l;e<s&&e<n;e++){const t=y+e;t>=0&&t<o&&(r[e]=i[t]*d[e])}}for(let u=l;u<n;u+=l){const p=Math.round(i(u/t)*t),m=T+l-g;let f=p;if(m>=0&&m+l<=x){let e=0;for(let t=0;t<l;t+=2){const n=b[m+t];e+=n*n}if(e>=1e-8){let t=0,n=-2;const i=-c,r=c;for(let a=i;a<=r;a+=4){const i=p+a;if(i<0||i+l>o)continue;const r=i-g;if(r<0||r+l>x)continue;let s=0,c=0;for(let e=0;e<l;e+=2){const t=b[m+e],n=b[r+e];s+=t*n,c+=n*n}const d=s/Math.sqrt(c*e+1e-12);d>n&&(n=d,t=a)}let a=t,s=-2;const d=Math.max(i,t-3),u=Math.min(r,t+3);for(let t=d;t<=u;t++){const n=p+t;if(n<0||n+l>o)continue;const i=n-g;if(i<0||i+l>x)continue;let r=0,c=0;for(let e=0;e<l;e++){const t=b[m+e],n=b[i+e];r+=t*n,c+=n*n}const d=r/Math.sqrt(c*e+1e-12);d>s&&(s=d,a=t)}f=p+a}}for(let t=0;t<r;t++){const i=e[t],r=a[t];for(let e=0;e<s;e++){const t=u+e;if(t>=n)break;const a=f+e;a>=0&&a<o&&(r[t]+=i[a]*d[e])}}T=f}return a}function stretchClipAudio(e,t,n){const i=n.sampleRate,r=n.numberOfChannels,a=Math.max(1,Math.ceil(t.duration*i)),o=[];for(let e=0;e<r;e++)o.push(n.getChannelData(e));const s=wsolaStretchChannels(o,i,a,e=>mediaTimeAt(t,t.start+e)),l=e.createBuffer(r,a,i);for(let e=0;e<r;e++)l.getChannelData(e).set(s[e]);return l}async function getExportAudioBuffer(e,t){if(!/^\/(?:projects\/|media\/|library\/)/.test(e.src))return getAudioBuffer(e).catch(()=>null);const n=await fetch(projectApi("/api/export/source-audio?src="+encodeURIComponent(e.src)),{signal:t});if(204===n.status)return null;if(!n.ok){const t=await n.json().catch(()=>({}));throw new Error(`Audio "${e.name||e.id}": ${t.error||"source decode failed"}`)}return getDecodeCtx().decodeAudioData(await n.arrayBuffer())}async function renderAudioMix(e,t){const n=[],i=new Map,r=new AbortController,a=()=>r.abort();let o;t?.addEventListener("abort",a,{once:!0}),t?.aborted&&a();try{for(const e of project.clips){if("audio"!==e.kind&&"video"!==e.kind)continue;if(!isTrackEnabled(e.track))continue;if(0===e.props?.volume&&!e.keyframes?.volume?.length)continue;const t=getMedia(e.mediaId);t&&(i.has(t.id)||i.set(t.id,getExportAudioBuffer(t,r.signal)),n.push(i.get(t.id).then(t=>t?{c:e,buf:t}:null)))}o=(await Promise.all(n)).filter(Boolean)}catch(e){throw a(),e}finally{t?.removeEventListener("abort",a)}if(!o.length)return null;const s=new OfflineAudioContext(2,Math.ceil(48e3*e)+1,48e3);for(const{c:e,buf:t}of o){const n=s.createBufferSource(),i=s.createGain(),r=Math.max(2,Math.ceil(30*e.duration)),a=new Float32Array(r);let o=0;for(let t=0;t<r;t++){const n=clamp(evalProps(e,e.start+t/(r-1)*e.duration).volume,0,4);a[t]=n,n>o&&(o=n)}i.gain.setValueCurveAtTime(a,Math.max(0,e.start),Math.max(.01,e.duration));const l=e.props?.audioChannel,{out:c}=connectChannelIsolated(s,n,i,l);c.connect(s.destination);const d=clipSpeed(e),u=hasSpeedRamp(e);(u||Math.abs(d-1)>1e-6)&&o>1e-6?(n.buffer=stretchClipAudio(s,e,t),n.playbackRate.value=1,n.start(Math.max(0,e.start),0,e.duration)):u?(n.buffer=t,n.playbackRate.value=1,n.start(Math.max(0,e.start),Math.max(0,e.in),e.duration)):(n.buffer=t,n.playbackRate.value=d,n.start(Math.max(0,e.start),Math.max(0,e.in),e.duration*d))}return encodeWAV(await s.startRendering())}async function prepareFrameAssets(e){for(const t of project.clips)if(activeAt(t,e)&&isTrackEnabled(t.track)&&("svg"===t.kind&&await prepareSvgFrame(t,e),t.props?.bgRemove&&("video"===t.kind||"image"===t.kind))){const e="video"===t.kind?optimizedSources?.get(t.id)||getClipEl(t):runtime.mediaAux.get(t.mediaId)?.img;if(e)try{await requestMask(t.id,optimizedSources?.has(t.id)?optimizedMaskSource(e):e,!0)}catch{}}}function readBrowserGpu(){try{const e=document.createElement("canvas"),t=e.getContext("webgl")||e.getContext("experimental-webgl");if(!t)return"disabled";const n=t.getExtension("WEBGL_debug_renderer_info");if(!n)return"unknown";const i=t.getParameter(n.UNMASKED_VENDOR_WEBGL)||"",r=t.getParameter(n.UNMASKED_RENDERER_WEBGL)||"";return String(i+"/"+r).slice(0,180)}catch{return""}}function reportExportDiagnostic(e,t){e&&fetch(projectApi("/api/export/report"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({requestId:e,diagnostic:!0,browserGpu:runtime.browserGpu,...t})}).catch(()=>{})}function installExportPageGuards(e){e&&!runtime.exportGuards&&(runtime.exportGuards=!0,runtime.browserGpu=readBrowserGpu(),window.addEventListener("error",t=>{reportExportDiagnostic(e,{error:t.message||"window error",stack:t.error?.stack,stage:"browser"})}),window.addEventListener("unhandledrejection",t=>{const n=t.reason;reportExportDiagnostic(e,{error:n?.message||String(n),stack:n?.stack,stage:"browser"})}),reportExportDiagnostic(e,{browserGpu:runtime.browserGpu}))}async function trackExportWait(e,t,n){const i=performance.now();try{return await n()}finally{const n=performance.now()-i;n>=250&&reportExportDiagnostic(e,{wait:{reason:t,elapsedMs:n},timeSec:state.time})}}async function fastExport(e={}){if(state.exporting)return;pause(),state.exporting=!0,state.rendering=!0,renderCancelled=!1,els.exportOverlay.classList.remove("hidden"),els.exportProgress.style.width="0%",els.exportNote.textContent=uiText("Rendering frames → ffmpeg. You can switch tabs; export continues.");const t=new AbortController;let n,i;fastExportAbort=t;try{installExportPageGuards(e.requestId);const r=exportWindow(e);exportDecodeBegin(t.signal,e.decode,e.requestId),els.exportTitle.textContent=uiText("Mixing audio…");const a=await trackExportWait(e.requestId,"audio-mix",()=>renderExportAudio(r.start,r.dur,t.signal)),o=await fetch(projectApi("/api/export/begin"),{method:"POST",body:JSON.stringify(exportBeginOptions(e,r,a)),signal:t.signal}),s=await o.json();if(!o.ok||!s.id)throw new Error(s.error||"export begin failed");if(n=s.id,a){const e=await fetch("/api/export/audio?id="+n,{method:"POST",body:a,signal:t.signal});if(!e.ok)throw new Error((await e.json()).error||"audio upload failed")}await loadLibraryFonts(),await document.fonts.ready,i=createExportPipeline(s,t.signal,async(e,i)=>{const r=await fetch(`/api/export/frame?id=${n}&frames=${i}`,{method:"POST",body:e,signal:t.signal});if(!r.ok)throw new Error((await r.json()).error||"frame upload failed")});for(let n=0;n<r.frames;n++){if(renderCancelled||t.signal.aborted)throw new Error("cancelled");const a=r.start+n/r.fps;state.time=a,await trackExportWait(e.requestId,"video-seek",()=>seekVideosTo(a)),await trackExportWait(e.requestId,"frame-assets",()=>prepareFrameAssets(a)),drawFrame(a),await i.pipeline.push(els.preview);const o=(n+1)/r.frames*100;els.exportProgress.style.width=o.toFixed(1)+"%",els.exportTitle.textContent=uiText("Rendering… {percent}%",{percent:o.toFixed(0)})}await i.pipeline.finish(),await i.queue.finish(),els.exportTitle.textContent=uiText("Encoding…");const l=await fetch("/api/export/end?id="+n,{method:"POST",signal:t.signal}).then(e=>e.json());if(!l.src)throw new Error(l.error||"encode failed");if(n=null,!e.requestId)for(const e of l.outputs||[l])if(e.src){const t=document.createElement("a");t.href=e.src,t.download=decodeURIComponent(e.src.split("/").pop()),t.click()}}catch(t){const n=renderCancelled||"AbortError"===t.name?"cancelled":String(t.message||t);e.requestId&&await fetch(projectApi("/api/export/report"),{method:"POST",body:JSON.stringify({requestId:e.requestId,group:e.group||0,error:n,stack:t.stack,timeSec:state.time,browserGpu:runtime.browserGpu})}).catch(()=>{}),e.requestId||"cancelled"===n||alert(uiText("Export failed: ")+n)}finally{i?.queue.cancel(),t.abort(),i?.encoder.close(),await(i?.pipeline.settle()),n&&await fetch("/api/export/end?id="+n+"&discard=1",{method:"POST"}).catch(()=>{}),exportDecodeEnd(),fastExportAbort=null,state.exporting=!1,state.rendering=!1,els.exportOverlay.classList.add("hidden"),runtime.pendingSync&&syncFromServer()}}let recorder=null,recChunks=[],recDiscard=!1;function pickMime(){return["video/mp4;codecs=avc1.42E01E,mp4a.40.2","video/mp4","video/webm;codecs=vp9,opus","video/webm;codecs=vp8,opus","video/webm"].find(e=>window.MediaRecorder&&MediaRecorder.isTypeSupported(e))||""}async function startExport(e=!1,t){if(state.exporting||realtimeStarting||realtimeFinishing)return;if(!project.clips.length)return void alert(uiText("Timeline is empty — add some clips first."));const n=pickMime();if(pause(),forgetBackgroundExport(),resetExportDialog(),e&&rememberExportDialog({engine:"local"}),updateExportProgress(0,uiText("Quick export in progress. Keep this page open and in the foreground.")),!n)return void failExportDialog(uiText("MediaRecorder is not supported in this browser."),!1);let i;state.exporting=!0,renderCancelled=!1,realtimeStarting=!0;try{if(ensureAudio(),await runtime.audio.ctx.resume(),await captureExportPoster(),!state.exporting||renderCancelled)return;if(state.time=0,resetPreviewWait(!0),seekMediaWhilePaused(),await new Promise(e=>setTimeout(e,350)),!state.exporting||renderCancelled)return;i=els.preview.captureStream(project.fps);for(const e of runtime.audio.recDest.stream.getAudioTracks())i.addTrack(e.clone());recChunks=[],recDiscard=!1,recorder=new MediaRecorder(i,{mimeType:n,videoBitsPerSecond:1e7}),recorder.ondataavailable=e=>{e.data.size&&recChunks.push(e.data)},recorder.onerror=e=>{finishExport(!1),failExportDialog(e.error?.message||e.type),showExportDialog()},recorder.onstop=()=>{for(const e of i.getTracks())e.stop();if(recorder=null,realtimeFinishing=!1,state.exporting=!1,runtime.pendingSync&&syncFromServer(),recDiscard)return void(recChunks=[]);if(!recChunks.length)return void failExportDialog(uiText("The exported file is unavailable. Please retry."));const e=n.startsWith("video/mp4")?"mp4":"webm";exportPreviewUrl=URL.createObjectURL(new Blob(recChunks,{type:n.split(";")[0]})),recChunks=[];let r=(t||exportDetails.name).replace(/[<>:"/\\|?*\x00-\x1f]+/g,"_").trim().replace(/\.(mp4|webm)$/i,"").slice(0,120).replace(/^[. ]+|[. ]+$/g,"")||"export";/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(r)&&(r="_"+r),completeExportDialog(exportPreviewUrl,r+"."+e)},runtime.audio.monitor.gain.value=0,recorder.start(250),resetPreviewWait(!0),lastTs=null,state.playing=!0,els.btnPlay.textContent="⏸",els.btnPlay.classList.add("on")}catch(e){finishExport(!1);for(const e of i?.getTracks()||[])e.stop();failExportDialog(e.message),showExportDialog()}finally{realtimeStarting=!1}}function finishExport(e){if(state.exporting&&!backgroundExportActive)if(realtimeFinishing)e||(recDiscard=!0,renderCancelled=!0,els.exportOverlay.classList.add("hidden"));else{realtimeFinishing=!(!recorder||"inactive"===recorder.state),state.exporting=realtimeFinishing,!realtimeFinishing&&runtime.pendingSync&&syncFromServer(),recDiscard=!e,renderCancelled=!e,state.playing=!1,els.btnPlay.textContent="▶",els.btnPlay.classList.remove("on");for(const e of runtime.clipEls.values())e.paused||e.pause();for(const e of runtime.clipGain.values())e.gain.value=0;runtime.audio&&(runtime.audio.monitor.gain.value=1),recorder&&"inactive"!==recorder.state&&recorder.stop(),e?updateExportProgress(99,uiText("Quick export in progress. Keep this page open and in the foreground.")):"error"!==els.exportOverlay.dataset.status&&els.exportOverlay.classList.add("hidden")}}async function runRealtimeJob(e,t){try{await startExport(!1,t);const e=Math.max(1,Math.round(projDur()*project.fps));let n=-1;for(;state.exporting;){const t=Math.min(e,Math.floor(state.time*project.fps));if(t!==n){const i=await fetch(projectApi(`/api/export/recording?frames=${t}&totalFrames=${e}`),{method:"POST"});if(!i.ok)throw new Error((await i.json()).error);n=t}await new Promise(e=>setTimeout(e,500))}if(!exportResult)throw new Error(els.exportNote.title||"Recording did not complete");const i=await(await fetch(exportResult.src)).blob(),r=await fetch(projectApi("/api/export/recording?name="+encodeURIComponent(exportResult.filename)),{method:"POST",body:i});if(!r.ok)throw new Error((await r.json()).error)}catch(t){await fetch(projectApi("/api/export/report"),{method:"POST",body:JSON.stringify({requestId:e,error:t.message,stack:t.stack,browserGpu:runtime.browserGpu})})}}function settingsFocusables(){const e=$("settingsDialog");return e?[...e.querySelectorAll("button, [href], input, select, textarea, [tabindex]:not([tabindex='-1'])")].filter(e=>!e.disabled&&e.getClientRects().length):[]}function onSettingsTabTrap(e){if("Tab"!==e.key)return;const t=settingsFocusables();if(!t.length)return;const n=t[0],i=t[t.length-1];e.shiftKey&&document.activeElement===n?(e.preventDefault(),i.focus()):e.shiftKey||document.activeElement!==i||(e.preventDefault(),n.focus())}function syncExportSettings(){$("setExportEngine").value=getSetting("exportEngine"),$("setExportProfile").value=getSetting("exportProfile"),$("setExportProfile").disabled="webcodecs"===getSetting("exportEngine")}function openSettings(){applyInterfaceVisibility();const e=$("setLinkSelect");e&&(e.checked=!!getSetting("linkSelect")),$("setLanguage").value=getSetting("language"),syncExportSettings();const t=$("settingsOverlay");t.classList.remove("hidden"),t.addEventListener("keydown",onSettingsTabTrap);const n=$("settingsDialog");($("setLanguage")||n)?.focus()}function closeSettings(){const e=$("settingsOverlay");e.removeEventListener("keydown",onSettingsTabTrap),e.classList.add("hidden"),($("btnSettings").getClientRects().length?$("btnSettings"):$("btnExport"))?.focus()}els.fileInput.addEventListener("change",()=>{importFiles(els.fileInput.files),els.fileInput.value=""}),$("btnTitle").addEventListener("click",addTitle),$("btnAdjust").addEventListener("click",addAdjust),$("btnSplit").addEventListener("click",splitAtPlayhead),$("btnUndo").addEventListener("click",undo),$("btnRedo").addEventListener("click",redo),$("btnCloseGap").addEventListener("click",()=>closeGapAtPlayhead()),$("btnNextGap").addEventListener("click",goToNextGap),$("btnTrimIO").addEventListener("click",trimToWorkArea),$("btnWorkAreaPlay").addEventListener("click",()=>{state.workAreaPlay=!state.workAreaPlay,syncTrimIOButton()}),$("btnDelete").addEventListener("click",()=>{clearFocusedTransition()||deleteSelected()}),$("btnExport").addEventListener("click",()=>{openExportButton()}),$("btnCloseExport").addEventListener("click",closeExportDialog),$("btnContinueExport").addEventListener("click",dismissExportCancellation),$("btnConfirmCancelExport").addEventListener("click",confirmCancelExport),$("cancelExportDialog").addEventListener("cancel",e=>{e.preventDefault(),dismissExportCancellation()}),$("cancelExportDialog").addEventListener("keydown",e=>{e.stopPropagation()}),$("btnRetryExport").addEventListener("click",()=>{openExportButton()}),$("btnExportFallback").addEventListener("click",()=>{!state.connected||["realtime","local"].includes(readExportDialog().engine)?startExport(!0):backgroundExport("realtime",{fallback:!0})}),$("btnSaveExport").addEventListener("click",()=>{saveCompletedExport()}),$("btnExportPreview").addEventListener("click",async()=>{const e=$("exportVideo");try{await e.play(),e.controls=!0,$("btnExportPreview").hidden=!0,$("exportTime").hidden=!0}catch{toast(uiText("Unable to preview. Open the downloaded video to watch it."))}}),$("exportVideo").addEventListener("ended",()=>{$("exportVideo").controls=!1,$("btnExportPreview").hidden=!1,$("exportTime").hidden=!1}),els.exportOverlay.addEventListener("keydown",e=>{if("Escape"===e.key&&(e.preventDefault(),e.stopPropagation(),closeExportDialog()),"Tab"!==e.key)return;const t=[...$("exportDialog").querySelectorAll("button, video[controls]")].filter(e=>!e.disabled&&e.getClientRects().length),n=t[0],i=t[t.length-1];e.shiftKey&&document.activeElement===n?(e.preventDefault(),i.focus()):e.shiftKey||document.activeElement!==i||(e.preventDefault(),n.focus())}),$("btnCancelExport").addEventListener("click",closeExportDialog),$("btnPlay").addEventListener("click",()=>state.playing?pause():play()),els.btnSpeed.addEventListener("click",()=>cyclePreviewRate(1)),$("btnHome").addEventListener("click",gotoHome),$("btnEnd").addEventListener("click",gotoEnd),$("btnBack").addEventListener("click",()=>setTime(state.time-1/project.fps)),$("btnFwd").addEventListener("click",()=>setTime(state.time+1/project.fps)),$("btnHelp").addEventListener("click",()=>$("helpOverlay").classList.remove("hidden")),$("btnCloseHelp").addEventListener("click",()=>$("helpOverlay").classList.add("hidden")),$("btnSettings").addEventListener("click",openSettings),$("btnCloseSettings").addEventListener("click",closeSettings),$("settingsOverlay").addEventListener("click",e=>{e.target===$("settingsOverlay")&&closeSettings()}),$("setExportProfile").addEventListener("change",e=>setSetting("exportProfile",e.target.value)),$("setExportEngine").addEventListener("change",e=>{setSetting("exportEngine",e.target.value),syncExportSettings()}),$("setLanguage").addEventListener("change",e=>{setSetting("language",FableCutI18n.normalize(e.target.value)),applyInterfaceLanguage()});for(const e of document.querySelectorAll("[data-visibility-setting]"))e.addEventListener("change",()=>setSetting(e.dataset.visibilitySetting,e.checked));function applyInterfaceLanguage(){FableCutI18n.setLanguage(getSetting("language")),FableCutI18n.apply(document),$("setLanguage").value=FableCutI18n.language,els.projectName.textContent=uiText(state.connected?"🟢 connected":"⚪ local session"),closeStylePicker(),runtime.mediaMenu?.close(),closeBinCtxMenu(),renderBin(),renderLibrary(),renderInspector();for(const e of TRACKS)syncTrackDisabledUI(e.id);state.dirtyTimeline=!0,syncAspectSel(),syncFpsSel(),syncExportTrackWarning(),buildMeterDOM(),els.btnPlay.title=uiText(runtime.previewWait?"Buffering…":"Play / Pause (Space)")}function syncSnapButton(){els.btnSnap.classList.toggle("on",state.snap),els.btnSnap.setAttribute("aria-pressed",String(state.snap)),els.btnSnap.dataset.i18nTitle=state.snap?"Snapping on (N)":"Snapping off (N)",els.btnSnap.title=uiText(els.btnSnap.dataset.i18nTitle)}function closeBinCtxMenu(){runtime.binCtxMenu&&(runtime.binCtxMenu.remove(),runtime.binCtxMenu=null,document.removeEventListener("pointerdown",onBinCtxDoc,!0))}function onBinCtxDoc(e){runtime.binCtxMenu&&!runtime.binCtxMenu.contains(e.target)&&closeBinCtxMenu()}function openProjectTabMenu(e,t){closeBinCtxMenu();const n=document.createElement("div");n.className="ctx-menu";const i=document.createElement("div");i.className="ctx-opt",i.textContent=uiText("New folder"),i.addEventListener("click",()=>{closeBinCtxMenu(),"project"!==state.binTab&&setBinTab("project"),addFolder(null)}),n.appendChild(i),document.body.appendChild(n);const r=n.offsetWidth,a=n.offsetHeight;n.style.left=Math.min(e,window.innerWidth-r-6)+"px",n.style.top=Math.min(t,window.innerHeight-a-6)+"px",runtime.binCtxMenu=n,document.addEventListener("pointerdown",onBinCtxDoc,!0)}function syncAspectSel(){if(!els.aspectSel)return;const e=ASPECT_PRESETS.findIndex(e=>e.w===project.width&&e.h===project.height);els.aspectSel.innerHTML=ASPECT_PRESETS.map((t,n)=>`<option value="${n}" ${n===e?"selected":""}>${uiText(t.label)}</option>`).join("")+(e<0?`<option value="custom" selected>${uiText("Custom · {width}×{height}",{width:project.width,height:project.height})}</option>`:"")}function syncFpsSel(){if(!els.fpsSel)return;const e=Number(project.fps),t=FPS_PRESETS.findIndex(t=>t===e);els.fpsSel.innerHTML=FPS_PRESETS.map((e,n)=>`<option value="${n}" ${n===t?"selected":""}>${e} fps</option>`).join("")+(t<0&&Number.isFinite(e)&&e>0?`<option value="custom" selected>${uiText("Custom · {fps} fps",{fps:e})}</option>`:"")}$("setLinkSelect").addEventListener("change",e=>{setSetting("linkSelect",!!e.target.checked),getSetting("linkSelect")?(selectedMediaIds().size&&"project"!==state.binTab&&setBinTab("project"),syncBinSelectionFromTimeline()):clearBinSelectionHighlight()}),els.btnSnap.addEventListener("click",()=>{state.snap=!state.snap,syncSnapButton()}),syncSnapButton(),els.btnAudioHold&&els.btnAudioHold.addEventListener("click",()=>setAudioHold(!state.audioHold)),$("btnLayoutReset").addEventListener("click",restoreDefaultLayout),$("trackSizeGroup").addEventListener("click",e=>{const t=e.target.closest("[data-track-size]");t&&setTrackSize(t.dataset.trackSize)}),els.binTabs.addEventListener("click",e=>{const t=e.target.closest("[data-tab]");t&&setBinTab(t.dataset.tab)}),els.binTabs.addEventListener("contextmenu",e=>{const t=e.target.closest("[data-tab]");t&&"project"===t.dataset.tab&&(e.preventDefault(),openProjectTabMenu(e.clientX,e.clientY))}),els.aspectSel.addEventListener("change",()=>{const e=ASPECT_PRESETS[+els.aspectSel.value];e&&(project.width=e.w,project.height=e.h,els.preview.width=e.w,els.preview.height=e.h,syncAspectSel(),seekMediaWhilePaused(),scheduleSave())}),els.fpsSel?.addEventListener("change",()=>{const e=FPS_PRESETS[+els.fpsSel.value];e>0&&(project.fps=e,syncFpsSel(),state.dirtyTimeline=!0,seekMediaWhilePaused(),scheduleSave())}),els.btnGuides.addEventListener("click",()=>{state.guides=!state.guides,els.btnGuides.classList.toggle("on",state.guides),els.safeOverlay.classList.toggle("hidden",!state.guides),state.guides&&updateSafeOverlay()});const VIEW_ZOOM_MIN=1,VIEW_PIXEL_MAX=2;let monitorFitCache=null,monitorViewPad={x:0,y:0};function measureMonitorFit(){const e=els.monitorStage.clientWidth,t=els.monitorStage.clientHeight,n=project.width||els.preview.width||1,i=project.height||els.preview.height||1,r=Math.min(e/n,t/i);return{w:n*r,h:i*r}}function monitorFitSize(){if(monitorFitCache)return monitorFitCache;const e=els.preview.offsetWidth,t=els.preview.offsetHeight;return e>0&&t>0&&state.viewZoom<=1.001?{w:e,h:t}:measureMonitorFit()}function maxViewZoom(){const{w:e}=monitorFitSize(),t=project.width||els.preview.width;return e&&t?Math.max(1,2*t/e):1}function applyMonitorView(){const e=state.viewZoom,t=e>1.001,n=els.monitorScroll,i=els.monitorZoomInner;if(t){monitorFitCache||(monitorFitCache=monitorFitSize());const t=monitorFitCache;els.preview.style.width=t.w*e+"px",els.preview.style.height=t.h*e+"px",monitorViewPad.x||monitorViewPad.y||(monitorViewPad={x:Math.ceil(els.monitorStage.clientWidth||n.clientWidth||0),y:Math.ceil(els.monitorStage.clientHeight||n.clientHeight||0)}),i&&(i.style.padding=`${monitorViewPad.y}px ${monitorViewPad.x}px`)}else state.viewZoom=1,monitorFitCache=null,monitorViewPad={x:0,y:0},els.preview.style.width="",els.preview.style.height="",i&&(i.style.padding=""),n.scrollLeft=0,n.scrollTop=0;els.btnZoom100.classList.toggle("hidden",!t),n.classList.toggle("is-zoomed",t),updateSafeOverlay()}let monitorViewRaf=0,monitorViewAfter=null;function scheduleMonitorView(e){e&&(monitorViewAfter=e),monitorViewRaf||(monitorViewRaf=requestAnimationFrame(()=>{monitorViewRaf=0;const e=monitorViewAfter;monitorViewAfter=null,applyMonitorView(),e&&e()}))}function resetMonitorView(){state.viewZoom=1,monitorFitCache=null,monitorViewPad={x:0,y:0},monitorViewAfter=null,monitorViewRaf&&(cancelAnimationFrame(monitorViewRaf),monitorViewRaf=0),applyMonitorView()}els.btnZoom100.addEventListener("click",resetMonitorView);let viewPanDrag=null;function endViewPan(e){if(viewPanDrag){viewPanDrag=null,els.monitorScroll.classList.remove("is-panning");try{els.monitorScroll.releasePointerCapture(e.pointerId)}catch{}}}function updateSafeOverlay(){if(!state.guides)return;const e=els.preview,t=els.safeOverlay.style;t.left=e.offsetLeft+"px",t.top=e.offsetTop+"px",t.width=e.offsetWidth+"px",t.height=e.offsetHeight+"px",els.safeOverlay.classList.toggle("vertical",project.height>project.width)}els.monitorScroll.addEventListener("pointerdown",e=>{if(!(state.viewZoom<=1.001)&&(1===e.button||0===e.button&&e.altKey)){const t=els.monitorScroll;viewPanDrag={x:e.clientX,y:e.clientY,sl:t.scrollLeft,st:t.scrollTop},t.classList.add("is-panning"),t.setPointerCapture(e.pointerId),e.preventDefault()}}),els.monitorScroll.addEventListener("pointermove",e=>{if(!viewPanDrag)return;const t=els.monitorScroll;t.scrollLeft=viewPanDrag.sl-(e.clientX-viewPanDrag.x),t.scrollTop=viewPanDrag.st-(e.clientY-viewPanDrag.y)}),els.monitorScroll.addEventListener("pointerup",endViewPan),els.monitorScroll.addEventListener("pointercancel",endViewPan),els.monitorScroll.addEventListener("auxclick",e=>{1===e.button&&e.preventDefault()}),els.monitorScroll.addEventListener("scroll",()=>{state.guides&&updateSafeOverlay(),scheduleMonitorView()}),"undefined"!=typeof ResizeObserver&&new ResizeObserver(()=>{if(state.viewZoom<=1.001)return monitorFitCache=null,void(state.guides&&updateSafeOverlay());const e=els.monitorScroll,t=e.scrollWidth>0?e.scrollLeft/e.scrollWidth:0,n=e.scrollHeight>0?e.scrollTop/e.scrollHeight:0;monitorFitCache=measureMonitorFit(),state.viewZoom>maxViewZoom()&&(state.viewZoom=maxViewZoom()),applyMonitorView(),e.scrollLeft=t*e.scrollWidth,e.scrollTop=n*e.scrollHeight}).observe(els.monitorStage),window.addEventListener("keydown",e=>{if($("coverDialog").open)return;const t=e.key;"Delete"!==t&&"Backspace"!==t||isTypingTarget(document.activeElement)||!clearFocusedTransition()?isTypingTarget(document.activeElement)||(" "===t?(e.preventDefault(),state.playing?pause():play()):"k"!==t&&"K"!==t||e.ctrlKey||e.metaKey||e.altKey?"l"!==t&&"L"!==t||e.ctrlKey||e.metaKey||e.altKey?"j"!==t&&"J"!==t||e.ctrlKey||e.metaKey||e.altKey?"s"===t||"S"===t?splitAtPlayhead():"g"!==t&&"G"!==t||e.ctrlKey||e.metaKey||e.altKey?!e.altKey||e.ctrlKey||e.metaKey||"t"!==t&&"T"!==t?"t"!==t&&"T"!==t||e.ctrlKey||e.metaKey||e.altKey?"Delete"===t||"Backspace"===t?deleteSelected():!e.ctrlKey&&!e.metaKey||e.altKey||"ArrowLeft"!==t&&"ArrowRight"!==t?"ArrowLeft"===t?setTime(state.time-(e.shiftKey?1:1/project.fps)):"ArrowRight"===t?setTime(state.time+(e.shiftKey?1:1/project.fps)):"Home"===t?gotoHome():"End"===t?gotoEnd():"["===t?trimToPlayhead("in"):"]"===t?trimToPlayhead("out"):"m"===t||"M"===t?toggleMarker():"i"!==t&&"I"!==t||e.ctrlKey||e.metaKey||e.altKey?"o"!==t&&"O"!==t||e.ctrlKey||e.metaKey||e.altKey?"n"===t||"N"===t?els.btnSnap.click():"Escape"===t?$("settingsOverlay").classList.contains("hidden")?$("helpOverlay").classList.contains("hidden")?selectClip(null):(e.preventDefault(),$("helpOverlay").classList.add("hidden")):(e.preventDefault(),closeSettings()):!e.ctrlKey&&!e.metaKey||"a"!==t&&"A"!==t?"+"===t||"="===t?setZoom(1.25*state.pps):"-"===t?setZoom(state.pps/1.25):"KeyZ"!==e.code||e.ctrlKey||e.metaKey?!e.ctrlKey&&!e.metaKey||"z"!==t&&"Z"!==t?!e.ctrlKey&&!e.metaKey||"y"!==t&&"Y"!==t||(e.preventDefault(),redo()):(e.preventDefault(),e.shiftKey?redo():undo()):(e.preventDefault(),e.altKey?zoomToWorkArea():e.shiftKey?zoomToFit():zoomToSelection()):(e.preventDefault(),setSelection(project.clips.map(e=>e.id))):(e.preventDefault(),e.shiftKey?clearOutPoint():setOutPoint()):(e.preventDefault(),e.shiftKey?clearInPoint():setInPoint()):(e.preventDefault(),goToKeyframe("ArrowRight"===t?1:-1)):(e.preventDefault(),e.shiftKey?trimToWorkArea():splitAtWorkArea()):(e.preventDefault(),addTransitionAtPlayhead()):(e.preventDefault(),e.shiftKey?closeGapAtPlayhead():goToNextGap()):(e.preventDefault(),state.playing?stepPreviewRate(-1):play()):(e.preventDefault(),state.playing?stepPreviewRate(1):play()):(e.preventDefault(),setPreviewRate(1),state.playing?pause():play())):e.preventDefault()}),window.addEventListener("resize",()=>{state.dirtyTimeline=!0,clampTimelineHeight()});const TL_H_KEY="fablecut-timeline-h",TL_H_MIN=180,UPPER_MIN=140;function availableTimelineMax(){const e=$("app").getBoundingClientRect(),t=document.querySelector(".topbar")?.getBoundingClientRect(),n=t?t.height:46;return Math.floor(e.height-n-140-6-18)}function setTimelineHeight(e){const t=clamp(Math.round(e),TL_H_MIN,Math.max(TL_H_MIN,availableTimelineMax()));return $("app").style.setProperty("--timeline-h",t+"px"),state.dirtyTimeline=!0,t}function defaultTimelineHeight(){const e=visibleTracks().reduce((e,t)=>e+t.h,0)||64,t=document.querySelector(".timeline-toolbar");return e+26+(t?t.getBoundingClientRect().height:40)+8}function resetTimelineHeight(){const e=setTimelineHeight(defaultTimelineHeight());return localStorage.removeItem(TL_H_KEY),state.dirtyTimeline=!0,e}function trackSizeShowsThumbs(){return!!(TRACK_SIZE_PRESETS[state.trackSize]||TRACK_SIZE_PRESETS.s).thumbs}function syncTrackSizeButtons(){const e=$("trackSizeGroup");if(e){for(const t of e.querySelectorAll("[data-track-size]"))t.classList.toggle("on",t.dataset.trackSize===state.trackSize);document.body.classList.toggle("track-size-s","s"===state.trackSize),document.body.classList.toggle("track-size-m","m"===state.trackSize),document.body.classList.toggle("track-size-l","l"===state.trackSize)}}function applyTrackHeights(){const e=TRACK_SIZE_PRESETS[state.trackSize]||TRACK_SIZE_PRESETS.s;for(const t of TRACKS){const n=e.h[t.id]??e.h["audio"===t.kind?"A1":"V1"];null!=n&&(t.h=n)}}function setTrackSize(e,{persist:t=!0,fitPane:n=!0}={}){if(getSetting("showLayoutControls")&&TRACK_SIZE_PRESETS[e]||(e="l"),state.trackSize=e,applyTrackHeights(),t&&localStorage.setItem(TRACK_SIZE_KEY,e),syncTrackSizeButtons(),buildTrackDOM(),state.dirtyTimeline=!0,rebuildClips(),n){const e=setTimelineHeight(defaultTimelineHeight());localStorage.setItem(TL_H_KEY,String(e))}}function restoreDefaultLayout(){setTrackSize("l",{persist:!0,fitPane:!1}),resetTimelineHeight()}function clampTimelineHeight(){const e=$("timelinePanel")?.getBoundingClientRect().height;e&&setTimelineHeight(e)}function initPanelSplit(){const e=$("splitUpperTimeline"),t=$("timelinePanel");if(!e||!t)return;state.trackSize="l",applyTrackHeights(),syncTrackSizeButtons();const n=parseFloat(localStorage.getItem(TL_H_KEY));n>0?setTimelineHeight(n):resetTimelineHeight(),e.addEventListener("pointerdown",n=>{if(0!==n.button)return;n.preventDefault(),e.setPointerCapture(n.pointerId),e.classList.add("dragging"),document.body.classList.add("resizing-panels");const i=n.clientY,r=t.getBoundingClientRect().height,a=e=>setTimelineHeight(r-(e.clientY-i)),o=()=>{e.releasePointerCapture(n.pointerId),e.classList.remove("dragging"),document.body.classList.remove("resizing-panels"),e.removeEventListener("pointermove",a),e.removeEventListener("pointerup",o),localStorage.setItem(TL_H_KEY,String(Math.round(t.getBoundingClientRect().height))),state.dirtyTimeline=!0};e.addEventListener("pointermove",a),e.addEventListener("pointerup",o)}),e.addEventListener("dblclick",e=>{e.preventDefault(),resetTimelineHeight()})}loadSettings(),applyInterfaceVisibility(),updateProjectTitle(),FableCutI18n.setLanguage(getSetting("language")),FableCutI18n.apply(document),$("setLanguage").value=getSetting("language"),syncAspectSel(),syncFpsSel(),initPanelSplit(),buildTrackDOM(),rebuildClips(),renderBin(),syncTrimIOButton(),buildMeterDOM(),connectServer().then(async()=>{await loadLibraryFonts();const e=new URLSearchParams(location.search),t=e.get("cliExport"),n=e.get("exportJob")||t;if(/^[a-f0-9]{32}$/.test(n||"")&&installExportPageGuards(n),state.connected&&/^[a-f0-9]{32}$/.test(t||"")){await new Promise(e=>requestAnimationFrame(()=>requestAnimationFrame(e)));const n=e.get("cliExportName")||void 0;"realtime"===e.get("cliExportEngine")&&e.get("exportJob")===t?await runRealtimeJob(t,n):await runExportPlan(t,n,e.get("cliExportEngine"))}else await restoreExportDialog()}),requestAnimationFrame(loop);
|