import { quantizeToFrameTimeS } from "../utils/frameTime.js";
import { getCloneFactory } from "./cloneFactoryRegistry.js";
import { EF_RENDERING } from "../EF_RENDERING.js";
import { efContext } from "../gui/efContext.js";
import { TWMixin } from "../gui/TWMixin2.js";
import { isTracingEnabled, withSpan } from "../otel/tracingHelpers.js";
import { FrameController, PRIORITY_DEFAULT } from "../preview/FrameController.js";
import { QualityUpgradeScheduler } from "../preview/QualityUpgradeScheduler.js";
import { updateAnimations } from "./updateAnimations.js";
import { parseTimeToMs } from "./parseTimeToMs.js";
import { __decorate } from "../_virtual/_@oxc-project_runtime@0.95.0/helpers/decorate.js";
import { EFTemporal, flushStartTimeMsCache, registerIsTimegroupCalculatingDuration, resetTemporalCache, shallowGetTemporalElements, timegroupContext } from "./EFTemporal.js";
import { renderTemporalAudio } from "./renderTemporalAudio.js";
import { EFTargetable } from "./TargetController.js";
import { deepGetMediaElements } from "./EFMedia.js";
import { TimegroupController } from "./TimegroupController.js";
import { getContainerInfoFromElement } from "./ContainerInfo.js";
import { getPositionInfoFromElement } from "./ElementPositionInfo.js";
import "../canvas/EFCanvas.js";
import "../gui/hierarchy/EFHierarchy.js";
import "../gui/EFFilmstrip.js";
import "../gui/EFFitScale.js";
import "../gui/EFWorkbench.js";
import "./EFPanZoom.js";
import { provide } from "@lit/context";
import debug from "debug";
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators.js";

//#region src/elements/EFTimegroup.ts
const log = debug("ef:elements:EFTimegroup");
const INITIALIZER_ERROR_THRESHOLD_MS = 2e3;
const INITIALIZER_WARN_THRESHOLD_MS = 100;
let durationCache = /* @__PURE__ */ new WeakMap();
const flushDurationCache = () => {
	durationCache = /* @__PURE__ */ new WeakMap();
};
const flushSequenceDurationCache = flushDurationCache;
const durationCalculationInProgress = /* @__PURE__ */ new WeakSet();
const isTimegroupCalculatingDuration = (timegroup) => {
	return timegroup !== void 0 && durationCalculationInProgress.has(timegroup);
};
registerIsTimegroupCalculatingDuration(isTimegroupCalculatingDuration);
/**
* Determines if a timegroup has its own duration based on its mode.
* This is the semantic rule: which modes produce independent durations.
*/
function hasOwnDurationForMode(mode, hasExplicitDuration) {
	return mode === "contain" || mode === "sequence" || mode === "fixed" && hasExplicitDuration;
}
/**
* Determines if a child temporal element should participate in parent duration calculation.
*
* Semantic rule: Fit-mode children inherit from parent, so they don't contribute to parent's
* duration calculation (to avoid circular dependencies). Children without own duration
* also don't contribute.
*/
function shouldParticipateInDurationCalculation(child) {
	if (child instanceof EFTimegroup && child.mode === "fit") return false;
	if (!child.hasOwnDuration) return false;
	return true;
}
/**
* Evaluates duration for "fit" mode: inherits from parent.
* Semantic rule: fit mode always matches parent duration, or 0 if no parent.
*/
function evaluateFitDuration(parentTimegroup) {
	if (!parentTimegroup) return 0;
	return parentTimegroup.durationMs;
}
/**
* Evaluates duration for "sequence" mode: sum of children minus overlaps.
* Semantic rule: sequence mode sums child durations, subtracting overlap between consecutive items.
* Fit-mode children are excluded to avoid circular dependencies.
*/
function evaluateSequenceDuration(timegroup, childTemporals, overlapMs) {
	const cachedDuration = durationCache.get(timegroup);
	if (cachedDuration !== void 0) return cachedDuration;
	let duration = 0;
	let participatingIndex = 0;
	childTemporals.forEach((child) => {
		if (!shouldParticipateInDurationCalculation(child)) return;
		if (child instanceof EFTimegroup && durationCalculationInProgress.has(child)) return;
		if (child instanceof EFTimegroup) {
			let ancestor = child.parentNode;
			let shouldSkip = false;
			while (ancestor) {
				if (ancestor === timegroup) break;
				if (ancestor instanceof EFTimegroup && durationCalculationInProgress.has(ancestor)) {
					shouldSkip = true;
					break;
				}
				ancestor = ancestor.parentNode;
			}
			if (shouldSkip) return;
		}
		if (participatingIndex > 0) duration -= overlapMs;
		duration += child.durationMs;
		participatingIndex++;
	});
	duration = Math.max(0, duration);
	durationCache.set(timegroup, duration);
	return duration;
}
/**
* Evaluates duration for "contain" mode: maximum of children.
* Semantic rule: contain mode takes the maximum child duration.
* Fit-mode children and children without own duration are excluded.
*/
function evaluateContainDuration(timegroup, childTemporals) {
	const cachedDuration = durationCache.get(timegroup);
	if (cachedDuration !== void 0) return cachedDuration;
	let maxDuration = 0;
	for (const child of childTemporals) {
		if (!shouldParticipateInDurationCalculation(child)) continue;
		if (child instanceof EFTimegroup && durationCalculationInProgress.has(child)) continue;
		if (child instanceof EFTimegroup) {
			let ancestor = child.parentNode;
			let shouldSkip = false;
			while (ancestor) {
				if (ancestor === timegroup) break;
				if (ancestor instanceof EFTimegroup && durationCalculationInProgress.has(ancestor)) {
					shouldSkip = true;
					break;
				}
				ancestor = ancestor.parentNode;
			}
			if (shouldSkip) continue;
		}
		maxDuration = Math.max(maxDuration, child.durationMs);
	}
	const duration = Math.max(0, maxDuration);
	durationCache.set(timegroup, duration);
	return duration;
}
/**
* Evaluates duration based on timegroup mode.
* This is the semantic evaluation function - it determines what duration should be.
*
* Note: Fixed mode is handled inline in the getter because it needs to call super.durationMs
* which requires the class context. The other modes are extracted for clarity.
*/
function evaluateDurationForMode(timegroup, mode, childTemporals) {
	switch (mode) {
		case "fit": return evaluateFitDuration(timegroup.parentTimegroup);
		case "sequence":
			durationCalculationInProgress.add(timegroup);
			try {
				return evaluateSequenceDuration(timegroup, childTemporals, timegroup.overlapMs);
			} finally {
				durationCalculationInProgress.delete(timegroup);
			}
		case "contain":
			durationCalculationInProgress.add(timegroup);
			try {
				return evaluateContainDuration(timegroup, childTemporals);
			} finally {
				durationCalculationInProgress.delete(timegroup);
			}
		default: throw new Error(`Invalid time mode: ${mode}`);
	}
}
const shallowGetTimegroups = (element, groups = []) => {
	for (const child of Array.from(element.children)) if (child instanceof EFTimegroup) groups.push(child);
	else shallowGetTimegroups(child, groups);
	return groups;
};
/**
* Evaluates the target time for a seek operation.
* Applies quantization and clamping to determine the valid seek target.
*/
function evaluateSeekTarget(requestedTime, durationMs, fps) {
	const quantizedTime = quantizeToFrameTimeS(requestedTime, fps);
	return Math.max(0, Math.min(quantizedTime, durationMs / 1e3));
}
let EFTimegroup = class EFTimegroup$1 extends EFTargetable(EFTemporal(TWMixin(LitElement))) {
	constructor(..._args) {
		super(..._args);
		this._timeGroupContext = this;
		this.efContext = this;
		this.fps = 30;
		this.autoInit = false;
		this.workbench = false;
		this.fit = "none";
		this.seekTask = (() => {
			const self = this;
			const taskObj = {
				run: () => {
					self.#seekTaskAbortController?.abort();
					self.#seekTaskAbortController = new AbortController();
					const signal = self.#seekTaskAbortController.signal;
					const targetTime = self.#pendingSeekTime ?? self.#currentTime;
					self.#seekTaskPromise = self.#runSeekTask(targetTime, signal);
					taskObj.taskComplete = self.#seekTaskPromise;
					return self.#seekTaskPromise;
				},
				taskComplete: Promise.resolve(void 0)
			};
			return taskObj;
		})();
	}
	static get observedAttributes() {
		return [
			...super.observedAttributes || [],
			"mode",
			"overlap",
			"currenttime",
			"fit",
			"fps",
			"auto-init",
			"workbench"
		];
	}
	static {
		this.styles = css`
    :host {
      display: block;
      position: relative;
      overflow: hidden;
    }

    ::slotted(ef-timegroup) {
      position: absolute;
      width: 100%;
      height: 100%;
      top: 0;
      left: 0;
      overflow: initial;
    }
  `;
	}
	#trackedChildren = /* @__PURE__ */ new Set();
	shouldAutoReady() {
		return false;
	}
	#childReadyStateHandler = () => {
		this.#recomputeAggregateReadyState();
	};
	#childContentChangeHandler = (e) => {
		const detail = e.detail;
		this.emitContentChange(detail?.reason ?? "content");
	};
	#recomputeAggregateReadyState() {
		const children = shallowGetTemporalElements(this);
		if (children.length === 0) {
			this.setContentReadyState("ready");
			return;
		}
		let hasLoading = false;
		let hasError = false;
		let hasIdle = false;
		for (const child of children) {
			const state$1 = child.contentReadyState;
			if (state$1 === "loading") hasLoading = true;
			else if (state$1 === "error") hasError = true;
			else if (state$1 === "idle") hasIdle = true;
		}
		if (hasError) this.setContentReadyState("error");
		else if (hasLoading) this.setContentReadyState("loading");
		else if (hasIdle) this.setContentReadyState("loading");
		else this.setContentReadyState("ready");
	}
	#syncChildListeners() {
		const currentChildren = new Set(shallowGetTemporalElements(this));
		for (const child of this.#trackedChildren) if (!currentChildren.has(child)) {
			child.removeEventListener("readystatechange", this.#childReadyStateHandler);
			child.removeEventListener("contentchange", this.#childContentChangeHandler);
		}
		for (const child of currentChildren) if (!this.#trackedChildren.has(child)) {
			child.addEventListener("readystatechange", this.#childReadyStateHandler);
			child.addEventListener("contentchange", this.#childContentChangeHandler);
		}
		this.#trackedChildren = currentChildren;
		this.#recomputeAggregateReadyState();
	}
	/** @public */
	#mode = "contain";
	get mode() {
		return this.#mode;
	}
	set mode(value) {
		if (this.#mode === value) return;
		const old = this.#mode;
		this.#mode = value;
		this.requestUpdate("mode", old);
		if (this.getAttribute("mode") !== value) this.setAttribute("mode", value);
	}
	/** @public */
	#overlapMs = 0;
	get overlapMs() {
		return this.#overlapMs;
	}
	set overlapMs(value) {
		if (this.#overlapMs === value) return;
		const old = this.#overlapMs;
		this.#overlapMs = value;
		this.requestUpdate("overlapMs", old);
		const attrVal = value > 0 ? `${value}ms` : null;
		if (attrVal && this.getAttribute("overlap") !== attrVal) this.setAttribute("overlap", attrVal);
		else if (!attrVal && this.hasAttribute("overlap")) this.removeAttribute("overlap");
	}
	#initializer;
	/**
	* Initializer function for setting up JavaScript behavior on this timegroup.
	* This function is called ONCE per instance - on the prime timeline when first connected,
	* and on each render clone when created.
	*
	* Use this to register frame callbacks, set up event listeners, or initialize state.
	* The same initializer code runs on both prime and clones, eliminating duplication.
	*
	* CONSTRAINTS:
	* - MUST be synchronous (no async/await, no Promise return)
	* - MUST complete in <100ms (error thrown) or <10ms (warning logged)
	* - Should only register callbacks and set up behavior, not do expensive work
	*
	* TIMING:
	* - If set before element connects to DOM: runs automatically after connectedCallback
	* - If set after element is connected: runs immediately
	* - Clones automatically copy and run the initializer when created
	*
	* @example
	* ```javascript
	* const tg = document.querySelector('ef-timegroup');
	* tg.initializer = (instance) => {
	*   // Runs once on prime timeline, once on each clone
	*   instance.addFrameTask((info) => {
	*     // Update content based on time
	*   });
	* };
	* ```
	* @public
	*/
	get initializer() {
		return this.#initializer;
	}
	set initializer(fn) {
		this.#initializer = fn;
	}
	/**
	* Track if initializer has run on this instance to prevent double execution.
	* @internal
	*/
	#initializerHasRun = false;
	attributeChangedCallback(name, old, value) {
		if (name === "mode" && value) this.mode = value;
		if (name === "overlap" && value) this.overlapMs = parseTimeToMs(value);
		if (name === "auto-init") this.autoInit = value !== null;
		if (name === "fps" && value) this.fps = Number.parseFloat(value);
		if (name === "workbench") this.workbench = value !== null;
		super.attributeChangedCallback(name, old, value);
	}
	#resizeObserver;
	/** Content epoch - increments when visual content changes (used by thumbnail cache) */
	#contentEpoch = 0;
	#currentTime = void 0;
	#userTimeMs = 0;
	#seekInProgress = false;
	#pendingSeekTime;
	#processingPendingSeek = false;
	#restoringFromLocalStorage = false;
	/** @internal */
	isRestoringFromLocalStorage() {
		return this.#restoringFromLocalStorage;
	}
	/** @internal - Used by PlaybackController to set restoration state */
	setRestoringFromLocalStorage(value) {
		this.#restoringFromLocalStorage = value;
	}
	#customFrameTasks = /* @__PURE__ */ new Set();
	#onFrameCallback = null;
	#onFrameCleanup = null;
	#playbackListener = null;
	/**
	* Centralized frame controller for coordinating element rendering.
	* Replaces the distributed Lit Task hierarchy with a single control point.
	*/
	#frameController = new FrameController(this);
	/**
	* Get the frame controller for centralized rendering coordination.
	* @public
	*/
	get frameController() {
		return this.#frameController;
	}
	/**
	* Centralized quality upgrade scheduler for coordinating main-quality segment fetching.
	* Lives alongside FrameController to manage background quality upgrades.
	*/
	#qualityUpgradeScheduler = new QualityUpgradeScheduler({ requestFrameRender: () => this.requestFrameRender() });
	/**
	* Get the quality upgrade scheduler for background segment fetching.
	* @public
	*/
	get qualityUpgradeScheduler() {
		return this.#qualityUpgradeScheduler;
	}
	/**
	* Query timegroup's readiness state for a given time.
	* Timegroups are always ready (no async preparation needed).
	* @public
	*/
	getFrameState(_timeMs) {
		return {
			needsPreparation: false,
			isReady: true,
			priority: PRIORITY_DEFAULT
		};
	}
	/**
	* Async preparation phase (no-op for timegroups).
	* Timegroups don't need preparation - they just coordinate child rendering.
	* @public
	*/
	async prepareFrame(_timeMs, _signal) {}
	/**
	* Synchronous render phase - executes custom frame callbacks.
	* Called by FrameController after all preparation is complete.
	* Kicks off async frame callbacks without blocking (they run in background).
	* @public
	*/
	renderFrame(_timeMs) {
		if (this.#customFrameTasks.size > 0) this.#executeCustomFrameTasks().catch((error) => {
			console.error("EFTimegroup custom frame task error:", error);
		});
	}
	/**
	* Get the effective FPS for this timegroup.
	* During rendering, uses the render options FPS if available.
	* Otherwise uses the configured fps property.
	* @public
	*/
	get effectiveFps() {
		if (typeof window !== "undefined" && window.EF_FRAMEGEN?.renderOptions) return window.EF_FRAMEGEN.renderOptions.encoderOptions.video.framerate;
		return this.fps;
	}
	/**
	* Get the current content epoch (used by thumbnail cache).
	* The epoch increments whenever visual content changes.
	* @public
	*/
	get contentEpoch() {
		return this.#contentEpoch;
	}
	/**
	* Increment content epoch (called when visual content changes).
	* This invalidates cached thumbnails by changing their cache keys.
	* @public
	*/
	incrementContentEpoch() {
		this.#contentEpoch++;
	}
	/**
	* Request a frame re-render at the current time.
	*
	* Use this when the source-to-timeline mapping has changed (e.g., sourcein/sourceout)
	* but currentTimeMs hasn't. The FrameController only re-renders when currentTimeMs
	* or durationMs change, so this provides a way for child elements to request a
	* re-render when their internal state changes the visual output.
	* @public
	*/
	requestFrameRender() {
		this.#frameController.abort();
		this.#runThrottledFrameTask();
	}
	async #runThrottledFrameTask() {
		if (this.playbackController) return this.playbackController.runThrottledFrameTask();
		try {
			await this.#frameController.renderFrame(this.currentTimeMs, { onAnimationsUpdate: (root) => {
				updateAnimations(root);
			} });
		} catch (error) {
			if (error instanceof DOMException && error.name === "AbortError") return;
			console.error("FrameController error:", error);
		}
	}
	/** @public */
	set currentTime(time) {
		const seekTarget = evaluateSeekTarget(time, this.durationMs, this.effectiveFps);
		if (this.playbackController) {
			this.playbackController.currentTime = seekTarget;
			this.#userTimeMs = seekTarget * 1e3;
			return;
		}
		if (!this.isRootTimegroup) return;
		if (Number.isNaN(seekTarget)) return;
		if (seekTarget === this.#currentTime && !this.#processingPendingSeek && !this.#restoringFromLocalStorage) return;
		if (this.#pendingSeekTime === seekTarget) return;
		if (this.#restoringFromLocalStorage && seekTarget !== this.#currentTime) {}
		if (this.#seekInProgress) {
			this.#pendingSeekTime = seekTarget;
			this.#currentTime = seekTarget;
			this._setLocalTimeMs(seekTarget * 1e3);
			this.#userTimeMs = seekTarget * 1e3;
			return;
		}
		this.#currentTime = seekTarget;
		this._setLocalTimeMs(seekTarget * 1e3);
		this.#userTimeMs = seekTarget * 1e3;
		this.#seekInProgress = true;
		Promise.resolve(this.seekTask.run()).catch(() => {}).finally(async () => {
			this.#seekInProgress = false;
			const { updateAnimations: updateAnimations$1 } = await import("./updateAnimations.js");
			updateAnimations$1(this);
			if (this.#pendingSeekTime !== void 0 && this.#pendingSeekTime !== seekTarget) {
				const pendingTime = this.#pendingSeekTime;
				this.#pendingSeekTime = void 0;
				this.#processingPendingSeek = true;
				try {
					this.currentTime = pendingTime;
				} finally {
					this.#processingPendingSeek = false;
				}
			} else this.#pendingSeekTime = void 0;
		});
	}
	/** @public */
	get currentTime() {
		if (this.playbackController) return this.playbackController.currentTime;
		return this.#currentTime ?? 0;
	}
	/** @public */
	set currentTimeMs(ms) {
		this.currentTime = ms / 1e3;
	}
	/** @public */
	get currentTimeMs() {
		return this.currentTime * 1e3;
	}
	/**
	* The time the user last requested via seek/scrub.
	* Preview systems should use this instead of currentTimeMs to avoid
	* seeing intermediate times during batch operations (thumbnails, export).
	* @public
	*/
	get userTimeMs() {
		return this.#userTimeMs;
	}
	/**
	* Seek to a specific time and wait for all frames to be ready.
	* This is the recommended way to seek in tests and programmatic control.
	*
	* Combines seeking (Purpose 3) with frame rendering (Purpose 4) to ensure
	* all visible elements are ready after the seek completes.
	*
	* Updates both the source time AND userTimeMs (what the preview displays).
	*
	* @param timeMs - Time in milliseconds to seek to
	* @returns Promise that resolves when the seek is complete and all visible children are ready
	* @public
	*/
	async seek(timeMs) {
		this.#userTimeMs = timeMs;
		this.currentTimeMs = timeMs;
		await this.seekTask.taskComplete;
		if (this.playbackController) this.saveTimeToLocalStorage(this.currentTime);
		await this.#frameController.renderFrame(timeMs, { onAnimationsUpdate: (root) => {
			updateAnimations(root);
		} });
	}
	/**
	* Optimized seek for render loops.
	* Unlike `seek()`, this:
	* - Skips waitForMediaDurations (already loaded at render setup)
	* - Skips localStorage persistence
	* - Uses FrameController for centralized element coordination
	*
	* Still waits for all content to be ready (Lit updates, element preparation, rendering).
	*
	* @param timeMs - Time in milliseconds to seek to
	* @internal
	*/
	async seekForRender(timeMs) {
		const t0 = performance.now();
		const newTime = timeMs / 1e3;
		this.#userTimeMs = timeMs;
		this.#currentTime = newTime;
		if (this.playbackController) this.playbackController.currentTime = newTime;
		this._setLocalTimeMs(timeMs);
		this.requestUpdate("currentTime");
		const t1 = performance.now();
		await this.updateComplete;
		const updateComplete1Ms = performance.now() - t1;
		const allLitElements = this.#getAllLitElementDescendants();
		const t2 = performance.now();
		await Promise.all(allLitElements.map((el) => el.updateComplete));
		const updateComplete2Ms = performance.now() - t2;
		const t3 = performance.now();
		await Promise.all(allLitElements.map((el) => el.updateComplete));
		const updateComplete3Ms = performance.now() - t3;
		const textElements = allLitElements.filter((el) => el.tagName === "EF-TEXT");
		const t4 = performance.now();
		if (textElements.length > 0) {
			await Promise.all(textElements.map((el) => {
				if ("whenSegmentsReady" in el && typeof el.whenSegmentsReady === "function") return el.whenSegmentsReady();
				return Promise.resolve();
			}));
			this.offsetHeight;
		}
		const textSegmentsMs = performance.now() - t4;
		const t5 = performance.now();
		const frameControllerTiming = await this.#frameController.renderFrame(timeMs, {
			waitForLitUpdate: false,
			onAnimationsUpdate: (root) => {
				updateAnimations(root);
				root.offsetWidth;
			}
		});
		const renderFrameMs = performance.now() - t5;
		const t6 = performance.now();
		await this.#executeCustomFrameTasks();
		const frameTasksMs = performance.now() - t6;
		const totalMs = performance.now() - t0;
		return {
			updateComplete1Ms,
			updateComplete2Ms,
			updateComplete3Ms,
			textSegmentsMs,
			renderFrameMs,
			renderFrameQueryMs: frameControllerTiming?.queryMs ?? 0,
			renderFramePrepareMs: frameControllerTiming?.prepareMs ?? 0,
			renderFrameDrawMs: frameControllerTiming?.renderMs ?? 0,
			renderFrameAnimsMs: frameControllerTiming?.animsMs ?? 0,
			frameTasksMs,
			totalMs
		};
	}
	/**
	* Collects all LitElement descendants recursively.
	* Used by seekForRender to ensure all reactive elements have updated.
	* Prunes subtrees of temporally-invisible elements — their Lit updates
	* still fire via microtasks (OwnCurrentTimeController), so skipping
	* the explicit await is safe.
	*/
	#getAllLitElementDescendants() {
		const result = [];
		const currentTimeMs = this.currentTimeMs;
		const walk = (el) => {
			for (const child of el.children) {
				if ("startTimeMs" in child && "endTimeMs" in child) {
					const startMs = child.startTimeMs ?? -Infinity;
					const endMs = child.endTimeMs ?? Infinity;
					if (endMs > startMs && (currentTimeMs < startMs || currentTimeMs >= endMs)) continue;
				}
				if (child instanceof LitElement) result.push(child);
				walk(child);
			}
		};
		walk(this);
		return result;
	}
	/**
	* Determines if this is a root timegroup (no parent timegroups)
	* @public
	*/
	get isRootTimegroup() {
		return !this.parentTimegroup;
	}
	/**
	* Property-based frame task callback for React integration.
	* When set, automatically registers the callback as a frame task.
	* Setting a new value automatically cleans up the previous callback.
	* Set to null or undefined to remove the callback.
	*
	* @example
	* // React usage:
	* <Timegroup onFrame={({ ownCurrentTimeMs, percentComplete }) => {
	*   // Per-frame updates
	* }} />
	*
	* @public
	*/
	get onFrame() {
		return this.#onFrameCallback;
	}
	set onFrame(callback) {
		if (this.#onFrameCleanup) {
			this.#onFrameCleanup();
			this.#onFrameCleanup = null;
		}
		this.#onFrameCallback = callback ?? null;
		if (callback) this.#onFrameCleanup = this.addFrameTask(callback);
	}
	/**
	* Register a custom frame task callback that will be executed during frame rendering.
	* The callback receives timing information and can be async or sync.
	* Multiple callbacks can be registered and will execute in parallel.
	*
	* @param callback - Function to execute on each frame
	* @returns A cleanup function that removes the callback when called
	* @public
	*/
	addFrameTask(callback) {
		if (typeof callback !== "function") throw new Error("Frame task callback must be a function");
		this.#customFrameTasks.add(callback);
		return () => {
			this.#customFrameTasks.delete(callback);
		};
	}
	/**
	* Remove a previously registered custom frame task callback.
	*
	* @param callback - The callback function to remove
	* @public
	*/
	removeFrameTask(callback) {
		this.#customFrameTasks.delete(callback);
	}
	/** @internal */
	saveTimeToLocalStorage(time) {
		try {
			if (this.id && this.isConnected && !Number.isNaN(time)) localStorage.setItem(this.storageKey, time.toString());
		} catch (error) {
			log("Failed to save time to localStorage", error);
		}
	}
	render() {
		return html`<slot @slotchange=${this.#handleSlotChange}></slot> `;
	}
	#handleSlotChange = () => {
		resetTemporalCache();
		flushSequenceDurationCache();
		flushStartTimeMsCache();
		this.requestUpdate();
		this.#syncChildListeners();
		this.emitContentChange("structure");
	};
	/** @internal */
	loadTimeFromLocalStorage() {
		if (this.id) try {
			const storedValue = localStorage.getItem(this.storageKey);
			if (storedValue === null) return;
			const parsedValue = Number.parseFloat(storedValue);
			if (Number.isNaN(parsedValue) || !Number.isFinite(parsedValue)) return;
			return parsedValue;
		} catch (error) {
			log("Failed to load time from localStorage", error);
		}
	}
	connectedCallback() {
		super.connectedCallback();
		for (const svg of this.querySelectorAll("svg")) svg.pauseAnimations();
		if (this.canvasPreviewActive) return;
		this.updateComplete.then(() => {
			this.#runInitializer();
			this.#syncChildListeners();
		});
		requestAnimationFrame(() => {
			requestAnimationFrame(() => {
				if (this.parentTimegroup) new TimegroupController(this.parentTimegroup, this);
				if (this.shouldWrapWithWorkbench()) this.wrapWithWorkbench();
			});
		});
	}
	/**
	* Called when this timegroup becomes a root (no parent timegroup).
	* Sets up the playback listener after PlaybackController is created.
	* @internal
	*/
	didBecomeRoot() {
		super.didBecomeRoot();
		this.#setupPlaybackListener();
		if (this.playbackController) fetch("https://editframe.com/api/v1/telemetry", {
			method: "POST",
			headers: { "Content-Type": "application/json" },
			body: JSON.stringify({ event_type: "load" }),
			keepalive: true
		}).catch(() => {});
	}
	/**
	* Setup listener on playbackController to sync userTimeMs during playback.
	*/
	#setupPlaybackListener() {
		if (this.#playbackListener || !this.playbackController) return;
		this.#playbackListener = (event) => {
			if (event.property === "currentTimeMs" && typeof event.value === "number") this.#userTimeMs = event.value;
		};
		this.playbackController.addListener(this.#playbackListener);
	}
	/**
	* Remove playback listener on disconnect.
	*/
	#removePlaybackListener() {
		if (this.#playbackListener && this.playbackController) this.playbackController.removeListener(this.#playbackListener);
		this.#playbackListener = null;
	}
	#previousDurationMs = 0;
	updated(changedProperties) {
		super.updated(changedProperties);
		if (changedProperties.has("mode") || changedProperties.has("overlapMs")) durationCache.delete(this);
		if (this.#previousDurationMs !== this.durationMs) {
			this.#previousDurationMs = this.durationMs;
			if (!this.hasAttribute("data-no-playback-controller")) this.#runThrottledFrameTask();
		}
	}
	disconnectedCallback() {
		super.disconnectedCallback();
		if (this.canvasPreviewActive) return;
		this.#resizeObserver?.disconnect();
		this.#removePlaybackListener();
		for (const child of this.#trackedChildren) {
			child.removeEventListener("readystatechange", this.#childReadyStateHandler);
			child.removeEventListener("contentchange", this.#childContentChangeHandler);
		}
		this.#trackedChildren.clear();
		this.#qualityUpgradeScheduler.dispose();
	}
	/**
	* Render the timegroup to an MP4 video file and trigger download.
	* Captures each frame at the specified fps, encodes using WebCodecs via
	* MediaBunny, and downloads the resulting video.
	*
	* Uses dynamic import to only load render utilities in browser context.
	*
	* @param options - Rendering options (fps, codec, bitrate, filename, etc.)
	* @returns Promise that resolves when video is downloaded
	* @public
	*/
	async renderToVideo(options) {
		const { renderTimegroupToVideo } = await import("../preview/renderTimegroupToVideo.js");
		return renderTimegroupToVideo(this, options);
	}
	/**
	* Runs the initializer function with validation for synchronous execution and time budget.
	* Only runs once per instance. Safe to call multiple times - will skip if already run.
	* @throws Error if initializer returns a Promise (async not allowed)
	* @throws Error if initializer takes more than INITIALIZER_ERROR_THRESHOLD_MS
	* @internal
	*/
	#runInitializer() {
		if (!this.initializer || this.#initializerHasRun) return;
		this.#initializerHasRun = true;
		const startTime = performance.now();
		const result = this.initializer(this);
		const elapsed = performance.now() - startTime;
		if (result !== void 0 && result !== null && typeof result.then === "function") throw new Error("Timeline initializer must be synchronous. Do not return a Promise from the initializer function.");
		if (elapsed > INITIALIZER_ERROR_THRESHOLD_MS) throw new Error(`Timeline initializer took ${elapsed.toFixed(1)}ms, exceeding the ${INITIALIZER_ERROR_THRESHOLD_MS}ms limit. Initializers must be fast - move expensive work outside the initializer.`);
		if (elapsed > INITIALIZER_WARN_THRESHOLD_MS) console.warn(`[ef-timegroup] Initializer took ${elapsed.toFixed(1)}ms, exceeding ${INITIALIZER_WARN_THRESHOLD_MS}ms. Consider optimizing for better render performance.`);
	}
	/**
	* Copy captionsData property from original to clone.
	* cloneNode() only copies attributes, not JavaScript properties.
	* captionsData is often set via JS (e.g., captionsEl.captionsData = {...}),
	* so we must manually copy it to the cloned elements.
	* @internal
	*/
	#copyCaptionsData(original, clone) {
		const originalCaptions = original.querySelectorAll("ef-captions");
		const cloneCaptions = clone.querySelectorAll("ef-captions");
		for (let i = 0; i < originalCaptions.length && i < cloneCaptions.length; i++) {
			const origCap = originalCaptions[i];
			const cloneCap = cloneCaptions[i];
			const loadedData = origCap.captionsData ?? origCap.unifiedCaptionsDataTask?.value;
			if (loadedData) cloneCap.captionsData = loadedData;
		}
	}
	/**
	* Copy ef-text _textContent property from original to cloned elements.
	* This MUST be called BEFORE elements upgrade (before updateComplete)
	* because splitText() runs in connectedCallback and will clear segments
	* if _textContent is null/empty.
	* @internal
	*/
	#copyTextContent(original, clone) {
		const originalTexts = original.querySelectorAll("ef-text");
		const cloneTexts = clone.querySelectorAll("ef-text");
		for (let i = 0; i < originalTexts.length && i < cloneTexts.length; i++) {
			const origText = originalTexts[i];
			const cloneText = cloneTexts[i];
			if (origText._textContent !== void 0) cloneText._textContent = origText._textContent;
			if (origText._templateElement !== void 0) cloneText._templateElement = origText._templateElement;
		}
	}
	/**
	* Copy ef-text-segment properties from original to cloned elements.
	* segmentText and other properties are set via JS, not attributes,
	* so we must manually copy them to the cloned elements.
	* @internal
	*/
	async #copyTextSegmentData(original, clone) {
		const originalSegments = original.querySelectorAll("ef-text-segment");
		const cloneSegments = clone.querySelectorAll("ef-text-segment");
		const updatePromises = [];
		for (let i = 0; i < originalSegments.length && i < cloneSegments.length; i++) {
			const origSeg = originalSegments[i];
			const cloneSeg = cloneSegments[i];
			if (origSeg.segmentText !== void 0) cloneSeg.segmentText = origSeg.segmentText;
			if (origSeg.segmentIndex !== void 0) cloneSeg.segmentIndex = origSeg.segmentIndex;
			if (origSeg.staggerOffsetMs !== void 0) cloneSeg.staggerOffsetMs = origSeg.staggerOffsetMs;
			if (origSeg.segmentStartMs !== void 0) cloneSeg.segmentStartMs = origSeg.segmentStartMs;
			if (origSeg.segmentEndMs !== void 0) cloneSeg.segmentEndMs = origSeg.segmentEndMs;
			if (cloneSeg.updateComplete) updatePromises.push(cloneSeg.updateComplete);
		}
		await Promise.all(updatePromises);
	}
	/**
	* Wait for all ef-captions elements to have their data loaded.
	* This is needed because EFCaptions is not an EFMedia, so waitForMediaDurations doesn't cover it.
	* Used by createRenderClone to ensure captions are ready before rendering.
	* @internal
	*/
	async #waitForCaptionsData(root) {
		const captionsElements = root.querySelectorAll("ef-captions");
		if (captionsElements.length === 0) return;
		const waitPromises = [];
		for (const el of captionsElements) {
			const captions = el;
			if (typeof captions.loadCaptionsData === "function") waitPromises.push(captions.loadCaptionsData().catch(() => {}));
			else if (captions.unifiedCaptionsDataTask?.taskComplete) waitPromises.push(captions.unifiedCaptionsDataTask.taskComplete.catch(() => {}));
		}
		if (waitPromises.length > 0) await Promise.all(waitPromises);
	}
	/**
	* Copies initializers from original timegroup tree to cloned timegroup tree.
	* Handles both the root timegroup and all nested timegroups recursively.
	* @internal
	*/
	async #copyInitializersToClone(original, clone) {
		if (original.initializer) {
			clone.initializer = original.initializer;
			await clone.updateComplete;
			clone.#runInitializer();
		}
		const originalNested = Array.from(original.querySelectorAll("ef-timegroup"));
		const cloneNested = Array.from(clone.querySelectorAll("ef-timegroup"));
		for (let i = 0; i < originalNested.length && i < cloneNested.length; i++) {
			const origNested = originalNested[i];
			const cloneNestedItem = cloneNested[i];
			if (origNested.initializer) {
				cloneNestedItem.initializer = origNested.initializer;
				await cloneNestedItem.updateComplete;
				cloneNestedItem.#runInitializer();
			}
		}
	}
	/**
	* Create an independent clone of this timegroup for rendering.
	* The clone is a fully functional ef-timegroup with its own animations
	* and time state, isolated from the original (Prime-timeline).
	*
	* OPTIONAL: An initializer can be set via `timegroup.initializer = (tg) => { ... }`
	* to re-run JavaScript setup (frame callbacks, React components) on each clone.
	*
	* This enables:
	* - Rendering without affecting user's preview position
	* - Concurrent renders with different clones
	* - Re-running JavaScript setup on each clone (if initializer is provided)
	*
	* @returns Promise resolving to clone, container, and cleanup function
	* @throws Error if initializer is async or takes too long
	* @public
	*/
	async createRenderClone() {
		const factory = getCloneFactory(this);
		if (factory) return this.#createRenderCloneFromFactory(factory);
		return this.#createRenderCloneFromDOM();
	}
	/**
	* Wait for all LitElement descendants to update and for text segments to be ready.
	* This ensures the clone is fully initialized before rendering.
	* @internal
	*/
	async #waitForDescendants(actualClone) {
		const allLitElements = Array.from(actualClone.querySelectorAll("*")).filter((el) => el instanceof LitElement);
		await Promise.all(allLitElements.map((el) => el.updateComplete));
		const textElements = allLitElements.filter((el) => el.tagName === "EF-TEXT");
		if (textElements.length > 0) {
			await Promise.all(textElements.map((el) => {
				if ("whenSegmentsReady" in el && typeof el.whenSegmentsReady === "function") return el.whenSegmentsReady();
				return Promise.resolve();
			}));
			actualClone.offsetHeight;
			await new Promise((resolve) => requestAnimationFrame(resolve));
		}
	}
	/**
	* Factory path: mount a fresh component tree (React, etc.) to produce
	* a fully functional clone. The factory is responsible for rendering
	* the component into the container and returning the root ef-timegroup.
	*/
	async #createRenderCloneFromFactory(factory) {
		const width = this.offsetWidth || 1920;
		const height = this.offsetHeight || 1080;
		const container = document.createElement("div");
		container.className = "ef-render-clone-container";
		container.style.cssText = `
      position: fixed;
      left: -9999px;
      top: 0;
      width: ${width}px;
      height: ${height}px;
      pointer-events: none;
      overflow: hidden;
    `;
		let renderTarget = container;
		const originalConfig = this.closest("ef-configuration");
		if (originalConfig) {
			const configClone = originalConfig.cloneNode(false);
			container.appendChild(configClone);
			renderTarget = configClone;
		}
		document.body.appendChild(container);
		const { timegroup: actualClone, cleanup: factoryCleanup } = factory(renderTarget);
		if (!actualClone) throw new Error("Clone factory did not produce an ef-timegroup. Ensure the factory renders a component containing a Timegroup.");
		actualClone.setAttribute("data-no-workbench", "true");
		actualClone.setAttribute("data-no-playback-controller", "true");
		actualClone.style.width = `${width}px`;
		actualClone.style.height = `${height}px`;
		actualClone.style.display = "block";
		await customElements.whenDefined("ef-timegroup");
		customElements.upgrade(container);
		await actualClone.updateComplete;
		await this.#waitForDescendants(actualClone);
		await this.#finalizeRenderClone(actualClone);
		return {
			clone: actualClone,
			container,
			cleanup: () => {
				container.remove();
				factoryCleanup();
			}
		};
	}
	/**
	* DOM path: deep clone the DOM tree and copy JavaScript properties.
	* Used for vanilla HTML/JS timelines that don't have a factory registered.
	*/
	async #createRenderCloneFromDOM() {
		const container = document.createElement("div");
		container.className = "ef-render-clone-container";
		container.style.cssText = `
      position: fixed;
      left: -9999px;
      top: 0;
      width: ${this.offsetWidth || 1920}px;
      height: ${this.offsetHeight || 1080}px;
      pointer-events: none;
      overflow: hidden;
    `;
		const cloneEl = this.cloneNode(true);
		cloneEl.removeAttribute("id");
		for (const el of cloneEl.querySelectorAll("[id]")) el.removeAttribute("id");
		cloneEl.setAttribute("data-no-workbench", "true");
		cloneEl.setAttribute("data-no-playback-controller", "true");
		const width = this.offsetWidth || 1920;
		const height = this.offsetHeight || 1080;
		cloneEl.style.width = `${width}px`;
		cloneEl.style.height = `${height}px`;
		cloneEl.style.display = "block";
		this.#copyCaptionsData(this, cloneEl);
		this.#copyTextContent(this, cloneEl);
		const originalConfig = this.closest("ef-configuration");
		if (originalConfig) {
			const configClone = originalConfig.cloneNode(false);
			configClone.appendChild(cloneEl);
			container.appendChild(configClone);
		} else container.appendChild(cloneEl);
		document.body.appendChild(container);
		await cloneEl.updateComplete;
		await this.#copyInitializersToClone(this, cloneEl);
		await this.#copyTextSegmentData(this, cloneEl);
		let actualClone = container.querySelector("ef-timegroup");
		if (!actualClone) throw new Error("No ef-timegroup found after initializer. Ensure your initializer renders a Timegroup (React) or does not remove the cloned element (vanilla JS).");
		await customElements.whenDefined("ef-timegroup");
		customElements.upgrade(container);
		actualClone = container.querySelector("ef-timegroup");
		if (!actualClone) throw new Error("ef-timegroup element lost after upgrade");
		await actualClone.updateComplete;
		await this.#waitForDescendants(actualClone);
		await this.#copyTextSegmentData(this, actualClone);
		await this.#finalizeRenderClone(actualClone);
		return {
			clone: actualClone,
			container,
			cleanup: () => {
				container.remove();
				const reactRoot = actualClone._reactRoot;
				if (reactRoot) queueMicrotask(() => {
					reactRoot.unmount();
				});
			}
		};
	}
	/**
	* Shared finalization for both factory and DOM clone paths:
	* - Set up parent-child temporal relationships
	* - Lock root timegroup references
	* - Wait for media durations and captions
	* - Remove PlaybackController
	* - Initial seek to frame 0
	*/
	async #finalizeRenderClone(actualClone) {
		const setupParentChildRelationships = (parent, root) => {
			for (const child of parent.children) if (child.tagName === "EF-TIMEGROUP") {
				const childTG = child;
				childTG.parentTimegroup = parent;
				childTG.rootTimegroup = root;
				childTG.lockRootTimegroup();
				setupParentChildRelationships(childTG, root);
			} else if ("parentTimegroup" in child && "rootTimegroup" in child) {
				const temporal = child;
				temporal.parentTimegroup = parent;
				temporal.rootTimegroup = root;
				if ("lockRootTimegroup" in temporal && typeof temporal.lockRootTimegroup === "function") temporal.lockRootTimegroup();
			} else if (child instanceof Element) setupInContainer(child, parent, root);
		};
		const setupInContainer = (container, nearestParentTG, root) => {
			for (const child of container.children) if (child.tagName === "EF-TIMEGROUP") {
				const childTG = child;
				childTG.parentTimegroup = nearestParentTG;
				childTG.rootTimegroup = root;
				childTG.lockRootTimegroup();
				setupParentChildRelationships(childTG, root);
			} else if ("parentTimegroup" in child && "rootTimegroup" in child) {
				const temporal = child;
				temporal.parentTimegroup = nearestParentTG;
				temporal.rootTimegroup = root;
				if ("lockRootTimegroup" in temporal && typeof temporal.lockRootTimegroup === "function") temporal.lockRootTimegroup();
			} else if (child instanceof Element) setupInContainer(child, nearestParentTG, root);
		};
		actualClone.rootTimegroup = actualClone;
		setupParentChildRelationships(actualClone, actualClone);
		await actualClone.updateComplete;
		actualClone.rootTimegroup = actualClone;
		actualClone.lockRootTimegroup();
		const finalizeRootTimegroup = (el) => {
			if ("rootTimegroup" in el && "lockRootTimegroup" in el) {
				el.rootTimegroup = actualClone;
				el.lockRootTimegroup();
			}
			for (const child of el.children) finalizeRootTimegroup(child);
		};
		finalizeRootTimegroup(actualClone);
		await actualClone.waitForMediaDurations();
		await this.#waitForCaptionsData(actualClone);
		if (actualClone.playbackController) {
			actualClone.playbackController.remove();
			actualClone.playbackController = void 0;
		}
		await actualClone.seek(0);
	}
	/** @internal */
	get storageKey() {
		if (!this.id) throw new Error("Timegroup must have an id to use localStorage.");
		return `ef-timegroup-${this.id}`;
	}
	/** @internal */
	get intrinsicDurationMs() {
		if (this.hasExplicitDuration) return this.explicitDurationMs;
	}
	/** @internal */
	get hasOwnDuration() {
		return hasOwnDurationForMode(this.mode, this.hasExplicitDuration);
	}
	/** @public */
	get durationMs() {
		if (this.mode === "fixed") return super.durationMs;
		const childTemporalsAsElements = this.childTemporals;
		return evaluateDurationForMode(this, this.mode, childTemporalsAsElements);
	}
	#mediaDurationsPromise = void 0;
	/** @internal */
	async waitForMediaDurations(signal) {
		signal?.throwIfAborted();
		if (!this.#mediaDurationsPromise) this.#mediaDurationsPromise = this.#waitForMediaDurations(signal).catch((err) => {
			if (err instanceof DOMException && err.name === "AbortError") {
				this.#mediaDurationsPromise = void 0;
				throw err;
			}
			console.error(`[EFTimegroup] waitForMediaDurations failed for ${this.id || "unnamed"}:`, err);
			this.#mediaDurationsPromise = void 0;
			throw err;
		});
		if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
		return this.#mediaDurationsPromise;
	}
	/**
	* Wait for all media elements to load their initial segments.
	* Ideally we would only need the extracted index json data, but
	* that caused issues with constructing audio data. We had negative durations
	* in calculations and it was not clear why.
	*/
	async #waitForMediaDurations(signal) {
		return withSpan("timegroup.waitForMediaDurations", {
			timegroupId: this.id || "unknown",
			mode: this.mode
		}, void 0, async (span) => {
			signal?.throwIfAborted();
			await new Promise((resolve, reject) => {
				if (signal?.aborted) {
					reject(new DOMException("Aborted", "AbortError"));
					return;
				}
				const abortHandler = () => {
					clearTimeout(timeoutId);
					cancelAnimationFrame(rafId2);
					cancelAnimationFrame(rafId1);
					reject(new DOMException("Aborted", "AbortError"));
				};
				signal?.addEventListener("abort", abortHandler, { once: true });
				let rafId1;
				let rafId2;
				let timeoutId;
				rafId1 = requestAnimationFrame(() => {
					if (signal?.aborted) {
						reject(new DOMException("Aborted", "AbortError"));
						return;
					}
					rafId2 = requestAnimationFrame(() => {
						if (signal?.aborted) {
							reject(new DOMException("Aborted", "AbortError"));
							return;
						}
						timeoutId = setTimeout(() => {
							signal?.removeEventListener("abort", abortHandler);
							resolve();
						}, 10);
					});
				});
			});
			signal?.throwIfAborted();
			const mediaElements = deepGetMediaElements(this);
			if (isTracingEnabled()) span.setAttribute("mediaElementsCount", mediaElements.length);
			signal?.throwIfAborted();
			const mediaLoadStart = Date.now();
			const MEDIA_LOAD_TIMEOUT_MS = 3e4;
			const loadPromises = mediaElements.map(async (m, index) => {
				signal?.throwIfAborted();
				const elementStart = Date.now();
				try {
					if (typeof m.getMediaEngine === "function") {
						const timeoutPromise = new Promise((_, reject) => {
							if (signal?.aborted) {
								reject(new DOMException("Aborted", "AbortError"));
								return;
							}
							const timeoutId = setTimeout(() => reject(/* @__PURE__ */ new Error(`Media element ${index} load timeout after ${MEDIA_LOAD_TIMEOUT_MS}ms`)), MEDIA_LOAD_TIMEOUT_MS);
							signal?.addEventListener("abort", () => {
								clearTimeout(timeoutId);
								reject(new DOMException("Aborted", "AbortError"));
							}, { once: true });
						});
						await Promise.race([m.getMediaEngine(signal), timeoutPromise]);
					} else if (m.mediaEngineTask) {
						const status = m.mediaEngineTask.status;
						if (status === 2 || status === 3) return;
						const taskPromise = m.mediaEngineTask.taskComplete;
						taskPromise?.catch(() => {});
						if (taskPromise) {
							const timeoutPromise = new Promise((_, reject) => {
								if (signal?.aborted) {
									reject(new DOMException("Aborted", "AbortError"));
									return;
								}
								const timeoutId = setTimeout(() => reject(/* @__PURE__ */ new Error(`Media element ${index} load timeout after ${MEDIA_LOAD_TIMEOUT_MS}ms`)), MEDIA_LOAD_TIMEOUT_MS);
								signal?.addEventListener("abort", () => {
									clearTimeout(timeoutId);
									reject(new DOMException("Aborted", "AbortError"));
								}, { once: true });
							});
							await Promise.race([taskPromise, timeoutPromise]);
						}
					}
				} catch (error) {
					if (error instanceof DOMException && error.name === "AbortError") throw error;
					if (isTracingEnabled()) {
						const elementElapsed = Date.now() - elementStart;
						console.error(`[EFTimegroup] Media element ${index} failed after ${elementElapsed}ms:`, error);
					}
				}
			});
			const results = await Promise.allSettled(loadPromises);
			if (results.some((r) => r.status === "rejected" && r.reason instanceof DOMException && r.reason.name === "AbortError")) throw new DOMException("Aborted", "AbortError");
			const failures = results.filter((r) => r.status === "rejected");
			if (failures.length > 0 && isTracingEnabled()) {
				const mediaLoadElapsed = Date.now() - mediaLoadStart;
				console.warn(`[EFTimegroup] ${failures.length} media elements failed to load in ${mediaLoadElapsed}ms:`, failures.map((r) => r.status === "rejected" ? r.reason : null));
			}
			flushStartTimeMsCache();
			flushSequenceDurationCache();
			setTimeout(() => this.requestUpdate("currentTime"), 0);
		});
	}
	/** @internal */
	get childTemporals() {
		return shallowGetTemporalElements(this);
	}
	/**
	* Returns true if the timegroup should be wrapped with a workbench.
	*
	* A timegroup should be wrapped with a workbench if:
	* - It's being rendered (EF_RENDERING), OR
	* - The workbench property is set to true
	*
	* If the timegroup is already wrapped in a context provider like ef-preview,
	* it should NOT be wrapped in a workbench.
	* @internal
	*/
	shouldWrapWithWorkbench() {
		if (this.canvasPreviewActive) return false;
		if (!this.isRootTimegroup) return false;
		if (this.closest("ef-canvas") !== null) return false;
		if (this.closest("ef-preview") !== null || this.closest("ef-workbench") !== null || this.closest("ef-preview-context") !== null) return false;
		if (this.closest("test-context") !== null || this.hasAttribute("data-no-workbench")) return false;
		if (EF_RENDERING?.() === true) return false;
		if (typeof window !== "undefined") {
			const params = new URLSearchParams(window.location.search);
			if (params.get("noWorkbench") === "true" || params.get("no-workbench") === "true") return false;
		}
		return this.workbench;
	}
	/** @internal */
	wrapWithWorkbench() {
		const workbench = document.createElement("ef-workbench");
		const parent = this.parentElement;
		if (EF_RENDERING()) {
			workbench.setAttribute("rendering", "");
			workbench.rendering = true;
		}
		if (parent === document.body) {
			workbench.style.position = "fixed";
			workbench.style.top = "0";
			workbench.style.left = "0";
			workbench.style.width = "100vw";
			workbench.style.height = "100vh";
			workbench.style.zIndex = "0";
		} else {
			workbench.style.position = "absolute";
			workbench.style.top = "0";
			workbench.style.left = "0";
			workbench.style.width = "100%";
			workbench.style.height = "100%";
		}
		parent?.append(workbench);
		if (!this.hasAttribute("id")) this.setAttribute("id", "root-timegroup");
		const panZoom = document.createElement("ef-pan-zoom");
		panZoom.id = "workbench-panzoom";
		panZoom.setAttribute("slot", "canvas");
		panZoom.setAttribute("auto-fit", "");
		panZoom.style.width = "100%";
		panZoom.style.height = "100%";
		const rect = this.getBoundingClientRect();
		const canvas = document.createElement("ef-canvas");
		canvas.id = "workbench-canvas";
		canvas.style.width = `${rect.width}px`;
		canvas.style.height = `${rect.height}px`;
		canvas.style.display = "block";
		canvas.append(this);
		panZoom.append(canvas);
		workbench.append(panZoom);
		const hierarchy = document.createElement("ef-hierarchy");
		hierarchy.setAttribute("slot", "hierarchy");
		hierarchy.setAttribute("target", "workbench-canvas");
		hierarchy.setAttribute("header", "Scenes");
		workbench.append(hierarchy);
		const filmstrip = document.createElement("ef-filmstrip");
		filmstrip.setAttribute("slot", "timeline");
		filmstrip.setAttribute("target", this.id);
		workbench.append(filmstrip);
	}
	/**
	* Returns media elements for playback audio rendering
	* For standalone media, returns [this]; for timegroups, returns all descendants
	* Used by PlaybackController for audio-driven playback
	* @internal
	*/
	getMediaElements() {
		return deepGetMediaElements(this);
	}
	/**
	* Render audio buffer for playback
	* Called by PlaybackController during live playback
	* Delegates to shared renderTemporalAudio utility for consistent behavior
	* @internal
	*/
	async renderAudio(fromMs, toMs, signal) {
		return renderTemporalAudio(this, fromMs, toMs, signal);
	}
	async #executeCustomFrameTasks() {
		if (this.#customFrameTasks.size > 0) {
			const percentComplete = this.durationMs > 0 ? this.ownCurrentTimeMs / this.durationMs : 0;
			const frameInfo = {
				ownCurrentTimeMs: this.ownCurrentTimeMs,
				currentTimeMs: this.currentTimeMs,
				durationMs: this.durationMs,
				percentComplete,
				element: this
			};
			await Promise.all(Array.from(this.#customFrameTasks).map((callback) => Promise.resolve(callback(frameInfo))));
		}
	}
	/** @internal */
	#seekTaskPromise = Promise.resolve(void 0);
	#seekTaskAbortController = null;
	async #runSeekTask(targetTime, signal) {
		try {
			signal.throwIfAborted();
			if (this.playbackController) {
				await this.playbackController.currentTime;
				signal.throwIfAborted();
				return this.currentTime;
			}
			if (!this.isRootTimegroup) return;
			return await withSpan("timegroup.seekTask", {
				timegroupId: this.id || "unknown",
				targetTime: targetTime ?? 0,
				durationMs: this.durationMs
			}, void 0, async (span) => {
				try {
					await Promise.race([this.waitForMediaDurations(signal), new Promise((_, reject) => {
						if (signal.aborted) {
							reject(new DOMException("Aborted", "AbortError"));
							return;
						}
						const timeoutId = setTimeout(() => reject(/* @__PURE__ */ new Error("waitForMediaDurations timeout")), 1e4);
						signal.addEventListener("abort", () => {
							clearTimeout(timeoutId);
							reject(new DOMException("Aborted", "AbortError"));
						}, { once: true });
					})]);
				} catch (error) {
					if (error instanceof DOMException && error.name === "AbortError") throw error;
				}
				signal.throwIfAborted();
				const newTime = evaluateSeekTarget(targetTime ?? 0, this.durationMs, this.effectiveFps);
				if (isTracingEnabled()) span.setAttribute("newTime", newTime);
				this.#currentTime = newTime;
				this._setLocalTimeMs(newTime * 1e3);
				this.requestUpdate("currentTime");
				await this.updateComplete;
				signal.throwIfAborted();
				await this.#runThrottledFrameTask();
				signal.throwIfAborted();
				if (!this.#restoringFromLocalStorage) this.saveTimeToLocalStorage(this.#currentTime);
				this.#seekInProgress = false;
				if (this.#restoringFromLocalStorage) this.#restoringFromLocalStorage = false;
				return newTime;
			});
		} catch (error) {
			if (error instanceof DOMException && error.name === "AbortError") return;
			console.error("EFTimegroup seekTask error", error);
			return;
		}
	}
	/**
	* Get container information for this timegroup.
	* Timegroups are always containers and can contain children.
	* Display mode is determined from computed styles.
	*
	* @public
	*/
	getContainerInfo() {
		return {
			...getContainerInfoFromElement(this),
			isContainer: true,
			canContainChildren: true
		};
	}
	/**
	* Get position information for this timegroup.
	* Returns computed bounds, transform, and rotation.
	*
	* @public
	*/
	getPositionInfo() {
		return getPositionInfoFromElement(this);
	}
};
__decorate([provide({ context: timegroupContext })], EFTimegroup.prototype, "_timeGroupContext", void 0);
__decorate([provide({ context: efContext })], EFTimegroup.prototype, "efContext", void 0);
__decorate([property({ type: Number })], EFTimegroup.prototype, "fps", void 0);
__decorate([property({
	type: Boolean,
	attribute: "auto-init"
})], EFTimegroup.prototype, "autoInit", void 0);
__decorate([property({
	type: Boolean,
	reflect: true
})], EFTimegroup.prototype, "workbench", void 0);
__decorate([property({ type: String })], EFTimegroup.prototype, "fit", void 0);
__decorate([property({
	type: Number,
	attribute: "currenttime"
})], EFTimegroup.prototype, "currentTime", null);
EFTimegroup = __decorate([customElement("ef-timegroup")], EFTimegroup);

//#endregion
export { EFTimegroup, flushSequenceDurationCache, shallowGetTimegroups };
//# sourceMappingURL=EFTimegroup.js.map