<script lang="ts">
	import { onMount, onDestroy } from 'svelte';
	import { tween } from '@animotion/motion';
	import { highlightCode } from './highlight/highlighter';
	import CodeMorph from './highlight/CodeMorph.svelte';
	import ParticlesBackground from './effects/ParticlesBackground.svelte';
	import ConfettiEffect from './effects/ConfettiEffect.svelte';
	import CounterRenderer from './renderers/CounterRenderer.svelte';
	import ChartRenderer from './renderers/ChartRenderer.svelte';
	import IconRenderer from './renderers/IconRenderer.svelte';
	import { easeInOutCubic, getEasingFn, getBackgroundStyle, hashFraction, getFloatAnimName, computeFloatAmp, computeFloatSpeed } from './engine/utils';
	import type {
		AnimotProject, AnimotPresenterProps, CanvasElement, CodeElement, TextElement,
		ArrowElement, ImageElement, ShapeElement, CounterElement, ChartElement, IconElement,
		SvgElement, MotionPathElement, PathPoint,
		Slide, CodeAnimationMode, AnimatableProperty
	} from './types';
	import './styles/presenter.css';

	type TweenValue = ReturnType<typeof tween<number>>;

	interface AnimatedElement {
		x: TweenValue; y: TweenValue; width: TweenValue; height: TweenValue;
		rotation: TweenValue; skewX: TweenValue; skewY: TweenValue;
		tiltX: TweenValue; tiltY: TweenValue; perspective: TweenValue;
		opacity: TweenValue; borderRadius: TweenValue;
		fontSize: TweenValue | null;
		fillColor: ReturnType<typeof tween<string>> | null;
		strokeColor: ReturnType<typeof tween<string>> | null;
		strokeWidth: TweenValue | null;
		shapeMorph: TweenValue | null;
		motionPathProgress: TweenValue | null;
		blur: TweenValue;
		brightness: TweenValue;
		contrast: TweenValue;
		saturate: TweenValue;
		grayscale: TweenValue;
	}

	interface ShapeMorphState { fromType: string; toType: string; }

	// Active motion path loop cancellation tokens
	let motionPathLoopAbort: AbortController | null = null;
	function cancelMotionPathLoops() {
		if (motionPathLoopAbort) { motionPathLoopAbort.abort(); motionPathLoopAbort = null; }
	}

	// --- Motion Path Utilities ---
	function buildPresenterPathD(points: PathPoint[], closed: boolean): string {
		if (points.length < 2) return '';
		let d = `M ${points[0].x} ${points[0].y}`;
		for (let i = 1; i < points.length; i++) {
			const prev = points[i - 1], curr = points[i];
			const cp1x = prev.x + (prev.handleOut?.x ?? 0), cp1y = prev.y + (prev.handleOut?.y ?? 0);
			const cp2x = curr.x + (curr.handleIn?.x ?? 0), cp2y = curr.y + (curr.handleIn?.y ?? 0);
			if (prev.handleOut || curr.handleIn) d += ` C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${curr.x} ${curr.y}`;
			else d += ` L ${curr.x} ${curr.y}`;
		}
		if (closed && points.length > 2) {
			const last = points[points.length - 1], first = points[0];
			const cp1x = last.x + (last.handleOut?.x ?? 0), cp1y = last.y + (last.handleOut?.y ?? 0);
			const cp2x = first.x + (first.handleIn?.x ?? 0), cp2y = first.y + (first.handleIn?.y ?? 0);
			if (last.handleOut || first.handleIn) d += ` C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${first.x} ${first.y}`;
			else d += ` Z`;
		}
		return d;
	}

	function cubicBez(p0: number, p1: number, p2: number, p3: number, t: number): number {
		const mt = 1 - t;
		return mt * mt * mt * p0 + 3 * mt * mt * t * p1 + 3 * mt * t * t * p2 + t * t * t * p3;
	}
	function cubicBezDeriv(p0: number, p1: number, p2: number, p3: number, t: number): number {
		const mt = 1 - t;
		return 3 * mt * mt * (p1 - p0) + 6 * mt * t * (p2 - p1) + 3 * t * t * (p3 - p2);
	}

	function getPresenterPointOnPath(points: PathPoint[], closed: boolean, progress: number): { x: number; y: number; angle: number } {
		if (points.length < 2) return { x: points[0]?.x ?? 0, y: points[0]?.y ?? 0, angle: 0 };
		const segs: { p0x: number; p0y: number; p1x: number; p1y: number; p2x: number; p2y: number; p3x: number; p3y: number; length: number }[] = [];
		const segCount = closed ? points.length : points.length - 1;
		for (let i = 0; i < segCount; i++) {
			const curr = points[i], next = points[(i + 1) % points.length];
			const p0x = curr.x, p0y = curr.y;
			const p1x = curr.x + (curr.handleOut?.x ?? 0), p1y = curr.y + (curr.handleOut?.y ?? 0);
			const p2x = next.x + (next.handleIn?.x ?? 0), p2y = next.y + (next.handleIn?.y ?? 0);
			const p3x = next.x, p3y = next.y;
			let length = 0, prevPx = p0x, prevPy = p0y;
			for (let s = 1; s <= 20; s++) {
				const t = s / 20;
				const px = cubicBez(p0x, p1x, p2x, p3x, t), py = cubicBez(p0y, p1y, p2y, p3y, t);
				length += Math.sqrt((px - prevPx) ** 2 + (py - prevPy) ** 2);
				prevPx = px; prevPy = py;
			}
			segs.push({ p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y, length });
		}
		const totalLength = segs.reduce((sum, s) => sum + s.length, 0);
		const targetLength = progress * totalLength;
		let accum = 0;
		for (const seg of segs) {
			if (accum + seg.length >= targetLength || seg === segs[segs.length - 1]) {
				const t = Math.max(0, Math.min(1, seg.length > 0 ? (targetLength - accum) / seg.length : 0));
				return {
					x: cubicBez(seg.p0x, seg.p1x, seg.p2x, seg.p3x, t),
					y: cubicBez(seg.p0y, seg.p1y, seg.p2y, seg.p3y, t),
					angle: Math.atan2(cubicBezDeriv(seg.p0y, seg.p1y, seg.p2y, seg.p3y, t), cubicBezDeriv(seg.p0x, seg.p1x, seg.p2x, seg.p3x, t)) * (180 / Math.PI)
				};
			}
			accum += seg.length;
		}
		return { x: points[0].x, y: points[0].y, angle: 0 };
	}

	let {
		src, data, autoplay = false, loop = false, controls = true, arrows = false,
		progress: showProgress = true, keyboard = true, duration: durationOverride,
		startSlide = 0, class: className = '', onslidechange, oncomplete
	}: AnimotPresenterProps = $props();

	// State
	let project = $state<AnimotProject | null>(null);
	let loading = $state(true);
	let error = $state<string | null>(null);
	let currentSlideIndex = $state(0);
	let isTransitioning = $state(false);
	let isAutoplay = $state(false);
	let transitionClass = $state('');
	let transitionDirection = $state<'forward' | 'backward'>('forward');
	let transitionDurationMs = $state(500);
	let containerEl: HTMLElement;
	let containerWidth = $state(0);
	let containerHeight = $state(0);

	let animatedElements = $state<Map<string, AnimatedElement>>(new Map());
	let codeHighlights = $state<Map<string, string>>(new Map());
	let elementContent = $state<Map<string, CanvasElement>>(new Map());
	let previousCodeContent = $state<Map<string, string>>(new Map());
	let codeMorphState = $state<Map<string, {oldCode: string, newCode: string, mode: CodeAnimationMode, speed: number, highlightColor: string}>>(new Map());
	let textTypewriterState = $state<Map<string, {fullText: string, displayedChars: number, isAnimating: boolean}>>(new Map());
	let typewriterIntervals = new Map<string, ReturnType<typeof setInterval>>();
	let shapeMorphStates = $state<Map<string, ShapeMorphState>>(new Map());
	let autoplayTimer: ReturnType<typeof setTimeout> | null = null;
	let menuVisible = $state(true);
	let mouseIdleTimer: ReturnType<typeof setTimeout> | null = null;

	const slides = $derived(project?.slides ?? []);
	const currentSlide = $derived(slides[currentSlideIndex]);
	const canvasWidth = $derived(currentSlide?.canvas.width ?? 800);
	const canvasHeight = $derived(currentSlide?.canvas.height ?? 600);

	const presentationScale = $derived.by(() => {
		if (!containerWidth || !containerHeight) return 1;
		const scaleX = containerWidth / canvasWidth;
		const scaleY = containerHeight / canvasHeight;
		return Math.min(scaleX, scaleY);
	});

	const backgroundStyle = $derived.by(() => {
		if (!currentSlide) return 'background: transparent';
		return getBackgroundStyle(currentSlide.canvas.background);
	});

	const allElementIds = $derived.by(() => {
		const ids = new Set<string>();
		slides.forEach(slide => slide.canvas.elements.forEach(el => ids.add(el.id)));
		return ids;
	});

	const sortedElementIds = $derived.by(() => {
		const elements: Array<{id: string, zIndex: number}> = [];
		for (const id of allElementIds) {
			const el = elementContent.get(id);
			if (el) elements.push({ id, zIndex: el.zIndex ?? 0 });
		}
		elements.sort((a, b) => a.zIndex - b.zIndex);
		return elements.map(e => e.id);
	});

	function getElementInSlide(slide: Slide | null, elementId: string): CanvasElement | undefined {
		return slide?.canvas.elements.find(el => el.id === elementId);
	}

	// Typewriter
	function startTypewriterAnimation(elementId: string, fullText: string, speed: number) {
		const existing = typewriterIntervals.get(elementId);
		if (existing) { clearInterval(existing); typewriterIntervals.delete(elementId); }
		textTypewriterState.set(elementId, { fullText, displayedChars: 0, isAnimating: true });
		textTypewriterState = new Map(textTypewriterState);
		const intervalMs = 1000 / speed;
		const interval = setInterval(() => {
			const state = textTypewriterState.get(elementId);
			if (state && state.isAnimating) {
				if (state.displayedChars < state.fullText.length) {
					textTypewriterState.set(elementId, { ...state, displayedChars: state.displayedChars + 1 });
					textTypewriterState = new Map(textTypewriterState);
				} else {
					clearInterval(interval); typewriterIntervals.delete(elementId);
					textTypewriterState.set(elementId, { ...state, isAnimating: false });
					textTypewriterState = new Map(textTypewriterState);
				}
			} else { clearInterval(interval); typewriterIntervals.delete(elementId); }
		}, intervalMs);
		typewriterIntervals.set(elementId, interval);
	}

	function clearAllTypewriterAnimations() {
		for (const [, interval] of typewriterIntervals) clearInterval(interval);
		typewriterIntervals.clear();
		textTypewriterState.clear();
		textTypewriterState = new Map(textTypewriterState);
	}

	// Build SVG path for 3+ control points using Catmull-Rom spline
	function buildCatmullRomPath(start: {x:number,y:number}, cps: {x:number,y:number}[], end: {x:number,y:number}): string {
		const pts = [start, ...cps, end];
		let d = `M ${pts[0].x} ${pts[0].y}`;
		for (let i = 0; i < pts.length - 1; i++) {
			const p0 = pts[i === 0 ? 0 : i - 1];
			const p1 = pts[i];
			const p2 = pts[i + 1];
			const p3 = pts[i + 2 < pts.length ? i + 2 : pts.length - 1];
			const c1x = p1.x + (p2.x - p0.x) / 6;
			const c1y = p1.y + (p2.y - p0.y) / 6;
			const c2x = p2.x - (p3.x - p1.x) / 6;
			const c2y = p2.y - (p3.y - p1.y) / 6;
			d += ` C ${c1x} ${c1y} ${c2x} ${c2y} ${p2.x} ${p2.y}`;
		}
		return d;
	}

	// Arrow draw/undraw/draw-undraw animation action
	function animateStyledArrowDraw(node: SVGPathElement, params: { enabled: boolean; mode: string; duration: number; dashPattern: string; startX: number; endX: number; slideIndex: number }) {
		let lastSlideIndex = params.slideIndex;
		let animationId: number | null = null;
		function runAnimation() {
			if (!params.enabled) return;
			if (animationId) cancelAnimationFrame(animationId);
			const svg = node.closest('svg') as SVGSVGElement | null;
			if (!svg) return;
			const goesLeftToRight = params.endX >= params.startX;
			const mode = params.mode;
			const dur = params.duration;
			const startTime = performance.now();
			if (mode === 'draw' || mode === 'draw-undraw') {
				svg.style.clipPath = goesLeftToRight ? 'inset(0 100% 0 0)' : 'inset(0 0 0 100%)';
			} else if (mode === 'undraw') {
				svg.style.clipPath = 'none';
			}
			function animate(currentTime: number) {
				const elapsed = currentTime - startTime;
				if (mode === 'draw') {
					const progress = Math.min(elapsed / dur, 1);
					const eased = 1 - Math.pow(1 - progress, 3);
					const inset = 100 * (1 - eased);
					svg!.style.clipPath = goesLeftToRight ? `inset(0 ${inset}% 0 0)` : `inset(0 0 0 ${inset}%)`;
					if (progress < 1) { animationId = requestAnimationFrame(animate); }
					else { svg!.style.clipPath = 'none'; animationId = null; }
				} else if (mode === 'undraw') {
					const progress = Math.min(elapsed / dur, 1);
					const eased = 1 - Math.pow(1 - progress, 3);
					const inset = 100 * eased;
					svg!.style.clipPath = goesLeftToRight ? `inset(0 0 0 ${inset}%)` : `inset(0 ${inset}% 0 0)`;
					if (progress < 1) { animationId = requestAnimationFrame(animate); }
					else { svg!.style.clipPath = 'inset(0 0 0 100%)'; animationId = null; }
				} else if (mode === 'draw-undraw') {
					const halfDur = dur / 2;
					if (elapsed < halfDur) {
						const progress = Math.min(elapsed / halfDur, 1);
						const eased = 1 - Math.pow(1 - progress, 3);
						const inset = 100 * (1 - eased);
						svg!.style.clipPath = goesLeftToRight ? `inset(0 ${inset}% 0 0)` : `inset(0 0 0 ${inset}%)`;
						animationId = requestAnimationFrame(animate);
					} else {
						const progress = Math.min((elapsed - halfDur) / halfDur, 1);
						const eased = 1 - Math.pow(1 - progress, 3);
						const inset = 100 * eased;
						svg!.style.clipPath = goesLeftToRight ? `inset(0 0 0 ${inset}%)` : `inset(0 ${inset}% 0 0)`;
						if (progress < 1) { animationId = requestAnimationFrame(animate); }
						else { svg!.style.clipPath = 'inset(0 0 0 100%)'; animationId = null; }
					}
				}
			}
			animationId = requestAnimationFrame(animate);
		}
		runAnimation();
		return {
			update(newParams: typeof params) {
				const slideChanged = newParams.slideIndex !== lastSlideIndex;
				params = newParams;
				if (!params.enabled) {
					if (animationId) { cancelAnimationFrame(animationId); animationId = null; }
					const svg = node.closest('svg') as SVGSVGElement | null;
					if (svg) svg.style.clipPath = '';
					lastSlideIndex = newParams.slideIndex;
					return;
				}
				if (slideChanged) { lastSlideIndex = newParams.slideIndex; runAnimation(); }
			},
			destroy() { if (animationId) cancelAnimationFrame(animationId); }
		};
	}

	// Init animated elements
	function initAllAnimatedElements() {
		const firstSlide = slides[0];
		if (firstSlide) {
			for (const element of firstSlide.canvas.elements) {
				if (element.type === 'code') previousCodeContent.set(element.id, (element as CodeElement).code);
				if (element.type === 'text') {
					const textEl = element as TextElement;
					if (textEl.animation?.mode === 'typewriter') startTypewriterAnimation(element.id, textEl.content, textEl.animation.typewriterSpeed || 50);
				}
			}
		}
		for (const slide of slides) {
			for (const element of slide.canvas.elements) {
				if (!animatedElements.has(element.id)) {
					const inCurrent = getElementInSlide(currentSlide, element.id);
					const startOpacity = inCurrent ? 1 : 0;
					const br = (element as any).borderRadius ?? 0;
					const isShape = element.type === 'shape';
					const shapeEl = isShape ? element as ShapeElement : null;
					const isText = element.type === 'text';
					const textEl = isText ? element as TextElement : null;
					animatedElements.set(element.id, {
						x: tween(element.position.x, { duration: 500 }),
						y: tween(element.position.y, { duration: 500 }),
						width: tween(element.size.width, { duration: 500 }),
						height: tween(element.size.height, { duration: 500 }),
						rotation: tween(element.rotation, { duration: 500 }),
						skewX: tween(element.skewX ?? 0, { duration: 500 }),
						skewY: tween(element.skewY ?? 0, { duration: 500 }),
						tiltX: tween(element.tiltX ?? 0, { duration: 500 }),
						tiltY: tween(element.tiltY ?? 0, { duration: 500 }),
						perspective: tween(element.perspective ?? 1000, { duration: 500 }),
						opacity: tween(startOpacity, { duration: 300 }),
						borderRadius: tween(br, { duration: 500 }),
						fontSize: textEl ? tween(textEl.fontSize, { duration: 500 }) : null,
						fillColor: shapeEl ? tween(shapeEl.fillColor, { duration: 500 }) : null,
						strokeColor: shapeEl ? tween(shapeEl.strokeColor, { duration: 500 }) : null,
						strokeWidth: shapeEl ? tween(shapeEl.strokeWidth, { duration: 500 }) : null,
						shapeMorph: shapeEl ? tween(1, { duration: 500 }) : null,
						motionPathProgress: element.motionPathConfig ? tween(0, { duration: 500 }) : null,
						blur: tween(element.blur ?? 0, { duration: 500 }),
						brightness: tween(element.brightness ?? 100, { duration: 500 }),
						contrast: tween(element.contrast ?? 100, { duration: 500 }),
						saturate: tween(element.saturate ?? 100, { duration: 500 }),
						grayscale: tween(element.grayscale ?? 0, { duration: 500 })
					});
					const currentSlideEl = getElementInSlide(currentSlide, element.id);
					elementContent.set(element.id, JSON.parse(JSON.stringify(currentSlideEl || element)));
				}
			}
		}
		animatedElements = new Map(animatedElements);
		elementContent = new Map(elementContent);
		previousCodeContent = new Map(previousCodeContent);
	}

	async function animateMotionPaths(slide: Slide) {
		cancelMotionPathLoops();
		motionPathLoopAbort = new AbortController();
		const signal = motionPathLoopAbort.signal;

		const resets: Promise<void>[] = [];
		for (const element of slide.canvas.elements) {
			if (element.motionPathConfig) {
				const animated = animatedElements.get(element.id);
				if (animated?.motionPathProgress) {
					resets.push(animated.motionPathProgress.to(0, { duration: 0 }));
				}
			}
		}
		await Promise.all(resets);
		for (const element of slide.canvas.elements) {
			if (element.motionPathConfig) {
				const animated = animatedElements.get(element.id);
				if (animated?.motionPathProgress) {
					const config = element.animationConfig;
					const duration = config?.duration ?? 2000;
					const easing = getEasingFn(config?.easing);
					const shouldLoop = element.motionPathConfig.loop;

					if (shouldLoop) {
						(async () => {
							while (!signal.aborted) {
								await animated.motionPathProgress!.to(0, { duration: 0 });
								await animated.motionPathProgress!.to(1, { duration, easing });
								if (!signal.aborted) await new Promise(r => setTimeout(r, 50));
							}
						})();
					} else {
						animated.motionPathProgress.to(1, { duration, easing });
					}
				}
			}
		}
	}

	// Animate to slide
	async function animateToSlide(targetIndex: number) {
		if (isTransitioning || targetIndex < 0 || targetIndex >= slides.length) return;
		if (targetIndex === currentSlideIndex) return;
		isTransitioning = true;
		transitionDirection = targetIndex > currentSlideIndex ? 'forward' : 'backward';
		const targetSlide = slides[targetIndex];
		clearAllTypewriterAnimations();
		cancelMotionPathLoops();
		const transition = targetSlide.transition;
		const duration = durationOverride ?? transition.duration;
		transitionDurationMs = duration;
		const hasSlideTransition = transition.type !== 'none';

		if (hasSlideTransition) {
			transitionClass = `transition-${transition.type}-out`;
			await new Promise(r => setTimeout(r, duration * 0.4));
			const newElementContent = new Map(elementContent);
			const newCodeMorphState = new Map(codeMorphState);
			const newPreviousCodeContent = new Map(previousCodeContent);
			for (const elementId of allElementIds) {
				const targetEl = getElementInSlide(targetSlide, elementId);
				const animated = animatedElements.get(elementId);
				if (targetEl) {
					if (targetEl.type === 'code') {
						const codeEl = targetEl as CodeElement;
						const prevCode = newPreviousCodeContent.get(elementId) || '';
						newCodeMorphState.set(elementId, { oldCode: prevCode, newCode: codeEl.code, mode: codeEl.animation?.mode || 'highlight-changes', speed: codeEl.animation?.typewriterSpeed || 50, highlightColor: codeEl.animation?.highlightColor || '#fef08a' });
						newPreviousCodeContent.set(elementId, codeEl.code);
					}
					newElementContent.set(elementId, JSON.parse(JSON.stringify(targetEl)));
					if (animated) {
						animated.x.to(targetEl.position.x, { duration: 0 }); animated.y.to(targetEl.position.y, { duration: 0 });
						animated.width.to(targetEl.size.width, { duration: 0 }); animated.height.to(targetEl.size.height, { duration: 0 });
						animated.rotation.to(targetEl.rotation, { duration: 0 });
						animated.skewX.to(targetEl.skewX ?? 0, { duration: 0 }); animated.skewY.to(targetEl.skewY ?? 0, { duration: 0 });
						animated.tiltX.to(targetEl.tiltX ?? 0, { duration: 0 }); animated.tiltY.to(targetEl.tiltY ?? 0, { duration: 0 });
						animated.perspective.to(targetEl.perspective ?? 1000, { duration: 0 });
						animated.opacity.to(1, { duration: 0 });
						animated.borderRadius.to((targetEl as any).borderRadius ?? 0, { duration: 0 });
						animated.blur.to(targetEl.blur ?? 0, { duration: 0 });
						animated.brightness.to(targetEl.brightness ?? 100, { duration: 0 });
						animated.contrast.to(targetEl.contrast ?? 100, { duration: 0 });
						animated.saturate.to(targetEl.saturate ?? 100, { duration: 0 });
						animated.grayscale.to(targetEl.grayscale ?? 0, { duration: 0 });
						if (targetEl.type === 'text') animated.fontSize?.to((targetEl as TextElement).fontSize, { duration: 0 });
						if (targetEl.type === 'shape') {
							const s = targetEl as ShapeElement;
							animated.fillColor?.to(s.fillColor, { duration: 0 });
							animated.strokeColor?.to(s.strokeColor, { duration: 0 });
							animated.strokeWidth?.to(s.strokeWidth, { duration: 0 });
						}
						if (animated.motionPathProgress) animated.motionPathProgress.to(0, { duration: 0 });
					}
				} else if (animated) { animated.opacity.to(0, { duration: 0 }); }
			}
			for (const [, element] of newElementContent) {
				if (element.type === 'code') {
					const codeEl = element as CodeElement;
					const key = `${codeEl.id}-${codeEl.code}-${codeEl.language}-${codeEl.showLineNumbers}`;
					if (!codeHighlights.has(key)) {
						const html = await highlightCode(codeEl.code, codeEl.language, codeEl.theme, { showLineNumbers: codeEl.showLineNumbers });
						codeHighlights.set(key, html);
					}
				}
			}
			codeHighlights = new Map(codeHighlights);
			shapeMorphStates = new Map();
			codeMorphState = newCodeMorphState;
			previousCodeContent = newPreviousCodeContent;
			elementContent = newElementContent;
			animatedElements = new Map(animatedElements);
			currentSlideIndex = targetIndex;
			for (const elementId of allElementIds) {
				const targetEl = getElementInSlide(targetSlide, elementId);
				if (targetEl?.type === 'text') {
					const textEl = targetEl as TextElement;
					if (textEl.animation?.mode === 'typewriter') startTypewriterAnimation(elementId, textEl.content, textEl.animation.typewriterSpeed || 50);
				}
			}
			transitionClass = `transition-${transition.type}-in`;
			await new Promise(r => setTimeout(r, duration * 0.6));
			transitionClass = '';
			animateMotionPaths(targetSlide);
			isTransitioning = false;
			onslidechange?.(targetIndex, slides.length);
			if (targetIndex === slides.length - 1 && !loop) oncomplete?.();
			return;
		}

		// Per-element morphing (transition type = 'none')
		const animations: Promise<void>[] = [];
		for (const elementId of allElementIds) {
			const currentEl = getElementInSlide(currentSlide, elementId);
			const animated = animatedElements.get(elementId);
			if (!animated) continue;
			if (currentEl) {
				await animated.x.to(currentEl.position.x, { duration: 0 });
				await animated.y.to(currentEl.position.y, { duration: 0 });
				await animated.width.to(currentEl.size.width, { duration: 0 });
				await animated.height.to(currentEl.size.height, { duration: 0 });
				await animated.rotation.to(currentEl.rotation, { duration: 0 });
				await animated.skewX.to(currentEl.skewX ?? 0, { duration: 0 });
				await animated.skewY.to(currentEl.skewY ?? 0, { duration: 0 });
				await animated.tiltX.to(currentEl.tiltX ?? 0, { duration: 0 });
				await animated.tiltY.to(currentEl.tiltY ?? 0, { duration: 0 });
				await animated.perspective.to(currentEl.perspective ?? 1000, { duration: 0 });
				await animated.borderRadius.to((currentEl as any).borderRadius ?? 0, { duration: 0 });
				await animated.blur.to(currentEl.blur ?? 0, { duration: 0 });
				await animated.brightness.to(currentEl.brightness ?? 100, { duration: 0 });
				await animated.contrast.to(currentEl.contrast ?? 100, { duration: 0 });
				await animated.saturate.to(currentEl.saturate ?? 100, { duration: 0 });
				await animated.grayscale.to(currentEl.grayscale ?? 0, { duration: 0 });
				await animated.opacity.to(1, { duration: 0 });
				if (currentEl.type === 'text' && animated.fontSize) await animated.fontSize.to((currentEl as TextElement).fontSize, { duration: 0 });
				if (currentEl.type === 'shape') {
					const s = currentEl as ShapeElement;
					if (animated.fillColor) await animated.fillColor.to(s.fillColor, { duration: 0 });
					if (animated.strokeColor) await animated.strokeColor.to(s.strokeColor, { duration: 0 });
					if (animated.strokeWidth) await animated.strokeWidth.to(s.strokeWidth, { duration: 0 });
				}
			}
		}

		// Update elementContent BEFORE animations start so rendered elements
		// (especially SVG viewBox) use target slide data while animating
		for (const elementId of allElementIds) {
			const targetEl = getElementInSlide(targetSlide, elementId);
			if (targetEl && targetEl.type !== 'code') {
				elementContent.set(elementId, JSON.parse(JSON.stringify(targetEl)));
			}
		}
		elementContent = new Map(elementContent);

		interface AnimationTask { elementId: string; order: number; delay: number; elementDuration: number; run: () => Promise<void>[]; }
		const animationTasks: AnimationTask[] = [];

		for (const elementId of allElementIds) {
			const currentEl = getElementInSlide(currentSlide, elementId);
			const targetEl = getElementInSlide(targetSlide, elementId);
			const animated = animatedElements.get(elementId);
			if (!animated) continue;
			const animConfig = targetEl?.animationConfig || currentEl?.animationConfig;
			const order = animConfig?.order ?? 0;
			const delay = animConfig?.delay ?? 0;
			const elementDuration = animConfig?.duration ?? duration;

			if (targetEl) {
				const easing = getEasingFn(animConfig?.easing);
				const propertySequences = targetEl.animationConfig?.propertySequences;
				if (targetEl.type === 'text') {
					const textEl = targetEl as TextElement;
					if (textEl.animation?.mode === 'typewriter') startTypewriterAnimation(elementId, textEl.content, textEl.animation.typewriterSpeed || 50);
				}

				const getSeqTiming = (prop: AnimatableProperty) => {
					if (!propertySequences?.length) return { duration: elementDuration, delay: 0, order: 0 };
					const seq = propertySequences.find(s => s.property === prop);
					return seq ? { duration: seq.duration, delay: seq.delay, order: seq.order } : { duration: elementDuration, delay: 0, order: 99 };
				};

				animationTasks.push({
					elementId, order, delay, elementDuration,
					run: () => {
						const anims: Promise<void>[] = [];
						if (propertySequences?.length) {
							const sequencedProps = new Set(propertySequences.map(s => s.property));
							if (!sequencedProps.has('position')) { anims.push(animated.x.to(targetEl.position.x, { duration: elementDuration, easing })); anims.push(animated.y.to(targetEl.position.y, { duration: elementDuration, easing })); }
							if (!sequencedProps.has('rotation')) anims.push(animated.rotation.to(targetEl.rotation, { duration: elementDuration, easing }));
							if (!sequencedProps.has('tilt')) { anims.push(animated.tiltX.to(targetEl.tiltX ?? 0, { duration: elementDuration, easing })); anims.push(animated.tiltY.to(targetEl.tiltY ?? 0, { duration: elementDuration, easing })); }
							if (!sequencedProps.has('skew')) { anims.push(animated.skewX.to(targetEl.skewX ?? 0, { duration: elementDuration, easing })); anims.push(animated.skewY.to(targetEl.skewY ?? 0, { duration: elementDuration, easing })); }
							if (!sequencedProps.has('size')) { anims.push(animated.width.to(targetEl.size.width, { duration: elementDuration, easing })); anims.push(animated.height.to(targetEl.size.height, { duration: elementDuration, easing })); }
							if (!sequencedProps.has('borderRadius')) anims.push(animated.borderRadius.to((targetEl as any).borderRadius ?? 0, { duration: elementDuration, easing }));
							if (!sequencedProps.has('blur')) {
								animated.blur.to(targetEl.blur ?? 0, { duration: elementDuration, easing });
								animated.brightness.to(targetEl.brightness ?? 100, { duration: elementDuration, easing });
								animated.contrast.to(targetEl.contrast ?? 100, { duration: elementDuration, easing });
								animated.saturate.to(targetEl.saturate ?? 100, { duration: elementDuration, easing });
								animated.grayscale.to(targetEl.grayscale ?? 0, { duration: elementDuration, easing });
							}
							if (!sequencedProps.has('perspective')) anims.push(animated.perspective.to(targetEl.perspective ?? 1000, { duration: elementDuration, easing }));
							const sortedSeqs = [...propertySequences].sort((a, b) => a.order - b.order);
							let cumulativeDelay = 0;
							for (const seq of sortedSeqs) {
								const seqDelay = cumulativeDelay + seq.delay;
								const seqDuration = seq.duration;
								setTimeout(() => {
									if (seq.property === 'position') { animated.x.to(targetEl.position.x, { duration: seqDuration, easing }); animated.y.to(targetEl.position.y, { duration: seqDuration, easing }); }
									else if (seq.property === 'rotation') animated.rotation.to(targetEl.rotation, { duration: seqDuration, easing });
									else if (seq.property === 'tilt') { animated.tiltX.to(targetEl.tiltX ?? 0, { duration: seqDuration, easing }); animated.tiltY.to(targetEl.tiltY ?? 0, { duration: seqDuration, easing }); }
									else if (seq.property === 'skew') { animated.skewX.to(targetEl.skewX ?? 0, { duration: seqDuration, easing }); animated.skewY.to(targetEl.skewY ?? 0, { duration: seqDuration, easing }); }
									else if (seq.property === 'size') { animated.width.to(targetEl.size.width, { duration: seqDuration, easing }); animated.height.to(targetEl.size.height, { duration: seqDuration, easing }); }
									else if (seq.property === 'borderRadius') animated.borderRadius.to((targetEl as any).borderRadius ?? 0, { duration: seqDuration, easing });
									else if (seq.property === 'blur') {
										animated.blur.to(targetEl.blur ?? 0, { duration: seqDuration, easing });
										animated.brightness.to(targetEl.brightness ?? 100, { duration: seqDuration, easing });
										animated.contrast.to(targetEl.contrast ?? 100, { duration: seqDuration, easing });
										animated.saturate.to(targetEl.saturate ?? 100, { duration: seqDuration, easing });
										animated.grayscale.to(targetEl.grayscale ?? 0, { duration: seqDuration, easing });
									}
									else if (seq.property === 'color' && targetEl.type === 'shape') {
										const s = targetEl as ShapeElement;
										animated.fillColor?.to(s.fillColor, { duration: seqDuration, easing });
										animated.strokeColor?.to(s.strokeColor, { duration: seqDuration, easing });
										animated.strokeWidth?.to(s.strokeWidth, { duration: seqDuration, easing });
									}
									else if (seq.property === 'perspective') animated.perspective.to(targetEl.perspective ?? 1000, { duration: seqDuration, easing });
								}, seqDelay);
								cumulativeDelay = seqDelay + seqDuration;
							}
							anims.push(new Promise(r => setTimeout(r, cumulativeDelay)));
						} else {
							anims.push(animated.x.to(targetEl.position.x, { duration: elementDuration, easing }));
							anims.push(animated.y.to(targetEl.position.y, { duration: elementDuration, easing }));
							anims.push(animated.width.to(targetEl.size.width, { duration: elementDuration, easing }));
							anims.push(animated.height.to(targetEl.size.height, { duration: elementDuration, easing }));
							anims.push(animated.rotation.to(targetEl.rotation, { duration: elementDuration, easing }));
							anims.push(animated.skewX.to(targetEl.skewX ?? 0, { duration: elementDuration, easing }));
							anims.push(animated.skewY.to(targetEl.skewY ?? 0, { duration: elementDuration, easing }));
							anims.push(animated.tiltX.to(targetEl.tiltX ?? 0, { duration: elementDuration, easing }));
							anims.push(animated.tiltY.to(targetEl.tiltY ?? 0, { duration: elementDuration, easing }));
							anims.push(animated.perspective.to(targetEl.perspective ?? 1000, { duration: elementDuration, easing }));
							anims.push(animated.borderRadius.to((targetEl as any).borderRadius ?? 0, { duration: elementDuration, easing }));
							anims.push(animated.blur.to(targetEl.blur ?? 0, { duration: elementDuration, easing }));
							anims.push(animated.brightness.to(targetEl.brightness ?? 100, { duration: elementDuration, easing }));
							anims.push(animated.contrast.to(targetEl.contrast ?? 100, { duration: elementDuration, easing }));
							anims.push(animated.saturate.to(targetEl.saturate ?? 100, { duration: elementDuration, easing }));
							anims.push(animated.grayscale.to(targetEl.grayscale ?? 0, { duration: elementDuration, easing }));
						}
						// Motion path progress — await reset, then animate forward
						if (animated.motionPathProgress && targetEl.motionPathConfig) {
							const shouldLoop = targetEl.motionPathConfig.loop;
							if (!shouldLoop) {
								anims.push((async () => {
									await animated.motionPathProgress!.to(0, { duration: 0 });
									await animated.motionPathProgress!.to(1, { duration: elementDuration, easing });
								})());
							}
						}
						if (targetEl.type === 'text' && animated.fontSize) anims.push(animated.fontSize.to((targetEl as TextElement).fontSize, { duration: elementDuration, easing }));
						if (targetEl.type === 'shape' && currentEl?.type === 'shape') {
							const ts = targetEl as ShapeElement;
							const cs = currentEl as ShapeElement;
							if (!propertySequences?.length) {
								if (animated.fillColor) anims.push(animated.fillColor.to(ts.fillColor, { duration: elementDuration, easing }));
								if (animated.strokeColor) anims.push(animated.strokeColor.to(ts.strokeColor, { duration: elementDuration, easing }));
								if (animated.strokeWidth) anims.push(animated.strokeWidth.to(ts.strokeWidth, { duration: elementDuration, easing }));
							}
							if (cs.shapeType !== ts.shapeType && animated.shapeMorph) {
								shapeMorphStates.set(elementId, { fromType: cs.shapeType, toType: ts.shapeType });
								shapeMorphStates = new Map(shapeMorphStates);
								anims.push(animated.shapeMorph.to(0, { duration: 0 }));
								anims.push(animated.shapeMorph.to(1, { duration: elementDuration, easing }));
							}
						} else if (targetEl.type === 'shape' && !propertySequences?.length) {
							const s = targetEl as ShapeElement;
							if (animated.fillColor) anims.push(animated.fillColor.to(s.fillColor, { duration: elementDuration, easing }));
							if (animated.strokeColor) anims.push(animated.strokeColor.to(s.strokeColor, { duration: elementDuration, easing }));
							if (animated.strokeWidth) anims.push(animated.strokeWidth.to(s.strokeWidth, { duration: elementDuration, easing }));
						}
						if (!currentEl) {
							// Snap ALL properties to target instantly — the tween may hold
							// stale values from a previous slide where the element last appeared
							anims.push(animated.x.to(targetEl.position.x, { duration: 0 }));
							anims.push(animated.y.to(targetEl.position.y, { duration: 0 }));
							anims.push(animated.width.to(targetEl.size.width, { duration: 0 }));
							anims.push(animated.height.to(targetEl.size.height, { duration: 0 }));
							anims.push(animated.rotation.to(targetEl.rotation, { duration: 0 }));
							anims.push(animated.skewX.to(targetEl.skewX ?? 0, { duration: 0 }));
							anims.push(animated.skewY.to(targetEl.skewY ?? 0, { duration: 0 }));
							anims.push(animated.tiltX.to(targetEl.tiltX ?? 0, { duration: 0 }));
							anims.push(animated.tiltY.to(targetEl.tiltY ?? 0, { duration: 0 }));
							anims.push(animated.perspective.to(targetEl.perspective ?? 1000, { duration: 0 }));
							anims.push(animated.borderRadius.to((targetEl as any).borderRadius ?? 0, { duration: 0 }));
							anims.push(animated.blur.to(targetEl.blur ?? 0, { duration: 0 }));
							anims.push(animated.brightness.to(targetEl.brightness ?? 100, { duration: 0 }));
							anims.push(animated.contrast.to(targetEl.contrast ?? 100, { duration: 0 }));
							anims.push(animated.saturate.to(targetEl.saturate ?? 100, { duration: 0 }));
							anims.push(animated.grayscale.to(targetEl.grayscale ?? 0, { duration: 0 }));
							if (targetEl.type === 'text' && animated.fontSize) {
								anims.push(animated.fontSize.to((targetEl as TextElement).fontSize, { duration: 0 }));
							}
							if (targetEl.type === 'shape') {
								const s = targetEl as ShapeElement;
								if (animated.fillColor) anims.push(animated.fillColor.to(s.fillColor, { duration: 0 }));
								if (animated.strokeColor) anims.push(animated.strokeColor.to(s.strokeColor, { duration: 0 }));
								if (animated.strokeWidth) anims.push(animated.strokeWidth.to(s.strokeWidth, { duration: 0 }));
							}
							const entrance = targetEl.animationConfig?.entrance ?? 'fade';
							if (entrance === 'fade') {
								anims.push(animated.opacity.to(1, { duration: elementDuration / 2, easing }));
							} else {
								anims.push(animated.opacity.to(1, { duration: 0 }));
							}
						}
						return anims;
					}
				});
			} else if (currentEl) {
				const exit = currentEl.animationConfig?.exit ?? 'fade';
				if (exit === 'fade') {
					const fadeOutDuration = Math.min(elementDuration / 2, 300);
					animationTasks.push({ elementId, order, delay, elementDuration, run: () => [animated.opacity.to(0, { duration: fadeOutDuration, easing: easeInOutCubic })] });
				} else {
					animationTasks.push({ elementId, order, delay: 0, elementDuration: 0, run: () => [animated.opacity.to(0, { duration: 0 })] });
				}
			}
		}

		animationTasks.sort((a, b) => a.order - b.order);
		const orderGroups = new Map<number, AnimationTask[]>();
		for (const task of animationTasks) {
			if (!orderGroups.has(task.order)) orderGroups.set(task.order, []);
			orderGroups.get(task.order)!.push(task);
		}
		const sortedOrders = [...orderGroups.keys()].sort((a, b) => a - b);
		for (let orderIdx = 0; orderIdx < sortedOrders.length; orderIdx++) {
			const order = sortedOrders[orderIdx];
			const tasks = orderGroups.get(order)!;
			const groupAnimations: Promise<void>[] = [];
			for (const task of tasks) {
				if (task.delay > 0) setTimeout(() => { task.run().forEach(p => animations.push(p)); }, task.delay);
				else groupAnimations.push(...task.run());
			}
			animations.push(...groupAnimations);
			if (orderIdx < sortedOrders.length - 1) {
				const maxDur = Math.max(...tasks.map(t => t.elementDuration));
				await new Promise(r => setTimeout(r, maxDur * 0.3));
			}
		}

		const newElementContent = new Map(elementContent);
		const newCodeMorphState = new Map(codeMorphState);
		const newPreviousCodeContent = new Map(previousCodeContent);
		for (const elementId of allElementIds) {
			const targetEl = getElementInSlide(targetSlide, elementId);
			if (targetEl) {
				if (targetEl.type === 'code') {
					const codeEl = targetEl as CodeElement;
					const prevCode = newPreviousCodeContent.get(elementId) || '';
					newCodeMorphState.set(elementId, { oldCode: prevCode, newCode: codeEl.code, mode: codeEl.animation?.mode || 'highlight-changes', speed: codeEl.animation?.typewriterSpeed || 50, highlightColor: codeEl.animation?.highlightColor || '#fef08a' });
					newPreviousCodeContent.set(elementId, codeEl.code);
				}
				newElementContent.set(elementId, JSON.parse(JSON.stringify(targetEl)));
			}
		}
		for (const [, element] of newElementContent) {
			if (element.type === 'code') {
				const codeEl = element as CodeElement;
				const key = `${codeEl.id}-${codeEl.code}-${codeEl.language}-${codeEl.showLineNumbers}`;
				if (!codeHighlights.has(key)) {
					const html = await highlightCode(codeEl.code, codeEl.language, codeEl.theme, { showLineNumbers: codeEl.showLineNumbers });
					codeHighlights.set(key, html);
				}
			}
		}
		codeHighlights = new Map(codeHighlights);
		shapeMorphStates = new Map();
		codeMorphState = newCodeMorphState;
		previousCodeContent = newPreviousCodeContent;
		elementContent = newElementContent;
		currentSlideIndex = targetIndex;
		isTransitioning = false;
		// Ensure elements not on the new slide are fully hidden
		const newSlide = slides[targetIndex];
		for (const elementId of allElementIds) {
			const onSlide = getElementInSlide(newSlide, elementId);
			const animated = animatedElements.get(elementId);
			if (!onSlide && animated) { animated.opacity.to(0, { duration: 0 }); }
		}
		animateMotionPaths(slides[targetIndex]);
		onslidechange?.(targetIndex, slides.length);
		if (targetIndex === slides.length - 1 && !loop) oncomplete?.();
	}

	// Autoplay
	function clearAutoplayTimer() { if (autoplayTimer) { clearTimeout(autoplayTimer); autoplayTimer = null; } }
	function scheduleNextSlide() {
		clearAutoplayTimer();
		if (!isAutoplay) return;
		const slideDuration = durationOverride ?? currentSlide?.duration ?? 3000;
		autoplayTimer = setTimeout(() => {
			if (currentSlideIndex < slides.length - 1) animateToSlide(currentSlideIndex + 1);
			else if (loop) animateToSlide(0);
			else isAutoplay = false;
		}, slideDuration);
	}
	$effect(() => { if (isAutoplay && !isTransitioning) scheduleNextSlide(); });
	$effect(() => () => clearAutoplayTimer());

	// Keyboard
	function handleKeyDown(e: KeyboardEvent) {
		if (!keyboard) return;
		if (e.key === 'ArrowRight' || e.key === ' ' || e.key === 'Enter') { e.preventDefault(); animateToSlide(currentSlideIndex + 1); }
		else if (e.key === 'ArrowLeft' || e.key === 'Backspace') { e.preventDefault(); animateToSlide(currentSlideIndex - 1); }
		else if (e.key === 'Home') animateToSlide(0);
		else if (e.key === 'End') animateToSlide(slides.length - 1);
		else if (e.key === 'p' || e.key === 'P') { isAutoplay = !isAutoplay; if (!isAutoplay) clearAutoplayTimer(); }
	}

	function resetMouseIdleTimer() {
		menuVisible = true;
		if (mouseIdleTimer) clearTimeout(mouseIdleTimer);
		mouseIdleTimer = setTimeout(() => { menuVisible = false; }, 3000);
	}

	// Code highlight helpers
	async function loadCodeHighlights() {
		for (const slide of slides) {
			for (const element of slide.canvas.elements) {
				if (element.type === 'code') {
					const codeEl = element as CodeElement;
					const key = `${codeEl.id}-${codeEl.code}-${codeEl.language}-${codeEl.showLineNumbers}`;
					if (!codeHighlights.has(key)) {
						const html = await highlightCode(codeEl.code, codeEl.language, codeEl.theme, { showLineNumbers: codeEl.showLineNumbers });
						codeHighlights.set(key, html);
					}
				}
			}
		}
		codeHighlights = new Map(codeHighlights);
	}

	function getCodeHighlight(elementId: string): string {
		const slideElement = getElementInSlide(currentSlide, elementId);
		if (!slideElement || slideElement.type !== 'code') return '';
		const codeEl = slideElement as CodeElement;
		const key = `${codeEl.id}-${codeEl.code}-${codeEl.language}-${codeEl.showLineNumbers}`;
		const cached = codeHighlights.get(key);
		if (cached) return cached;
		highlightCode(codeEl.code, codeEl.language, codeEl.theme, { showLineNumbers: codeEl.showLineNumbers }).then(html => {
			codeHighlights.set(key, html);
			codeHighlights = new Map(codeHighlights);
		});
		return '';
	}

	// Public API (exposed via bind:this)
	export async function goto(slideIndex: number) { await animateToSlide(slideIndex); }
	export async function next() { await animateToSlide(currentSlideIndex + 1); }
	export async function prev() { await animateToSlide(currentSlideIndex - 1); }
	export function play() { isAutoplay = true; }
	export function pause() { isAutoplay = false; clearAutoplayTimer(); }
	export function getCurrentSlide() { return currentSlideIndex; }
	export function getTotalSlides() { return slides.length; }
	export function getIsPlaying() { return isAutoplay; }

	// Auto-load Google Fonts used by text elements in the project.
	// Generic CSS font families that don't need loading
	const GENERIC_FONTS = new Set([
		'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
		'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
		'math', 'emoji', 'fangsong', 'inherit', 'initial', 'unset'
	]);

	// Extract individual font names from a CSS font-family string.
	// e.g. '"JetBrains Mono", system-ui, monospace' → ['JetBrains Mono']
	function extractFontNames(fontFamily: string): string[] {
		return fontFamily
			.split(',')
			.map(f => f.trim().replace(/^['"]|['"]$/g, ''))
			.filter(f => f && !GENERIC_FONTS.has(f.toLowerCase()));
	}

	// Auto-load fonts used by text/counter elements.
	// Uses fontsource CDN (jsDelivr) which registers the SAME font-family names
	// as the app (e.g. "Plus Jakarta Sans Variable"), unlike Google Fonts which
	// strips the "Variable" suffix.
	function loadProjectFonts(proj: AnimotProject) {
		const fonts = new Set<string>();
		for (const slide of proj.slides) {
			for (const el of slide.canvas.elements) {
				if (el.type === 'text' || el.type === 'counter') {
					const f = (el as any).fontFamily as string | undefined;
					if (f) {
						for (const name of extractFontNames(f)) fonts.add(name);
					}
				}
			}
		}
		if (fonts.size === 0) return;

		// Deduplicate against already-injected links to avoid double-loading
		const loaded = new Set<string>();
		document.querySelectorAll<HTMLLinkElement>('link[data-animot-font]').forEach(l => loaded.add(l.dataset.animotFont!));

		for (const font of fonts) {
			if (loaded.has(font)) continue;
			const isVariable = /\s+Variable$/i.test(font);
			// Convert font name to fontsource package slug:
			// "Plus Jakarta Sans Variable" → "plus-jakarta-sans"
			// "JetBrains Mono" → "jetbrains-mono"
			const baseName = font.replace(/\s*Variable$/i, '');
			const slug = baseName.toLowerCase().replace(/\s+/g, '-');
			const pkg = isVariable
				? `@fontsource-variable/${slug}`
				: `@fontsource/${slug}`;
			const link = document.createElement('link');
			link.rel = 'stylesheet';
			link.href = `https://cdn.jsdelivr.net/npm/${pkg}/index.css`;
			link.dataset.animotFont = font;
			document.head.appendChild(link);
		}
	}

	// Load data
	async function loadProject() {
		loading = true; error = null;
		try {
			if (data) { project = data; }
			else if (src) {
				const res = await fetch(src);
				if (!res.ok) throw new Error(`Failed to load: ${res.status}`);
				project = await res.json();
			} else { throw new Error('Either src or data prop is required'); }
			loadProjectFonts(project!);
			currentSlideIndex = startSlide;
			await new Promise(r => setTimeout(r, 10));
			initAllAnimatedElements();
			await loadCodeHighlights();
			loading = false;
			if (currentSlide) setTimeout(() => animateMotionPaths(currentSlide!), 300);
			if (autoplay) isAutoplay = true;
		} catch (e: any) { error = e.message; loading = false; }
	}

	// ResizeObserver
	let resizeObserver: ResizeObserver;

	onMount(() => {
		loadProject();
		resizeObserver = new ResizeObserver(entries => {
			for (const entry of entries) {
				containerWidth = entry.contentRect.width;
				containerHeight = entry.contentRect.height;
			}
		});
		if (containerEl) resizeObserver.observe(containerEl);
		resetMouseIdleTimer();
		return () => { resizeObserver?.disconnect(); clearAutoplayTimer(); clearAllTypewriterAnimations(); if (mouseIdleTimer) clearTimeout(mouseIdleTimer); };
	});

	// Watch for prop changes
	$effect(() => { if (data) { project = data; } });
</script>

<svelte:window onkeydown={handleKeyDown} />

<div
	class="animot-presenter {className}"
	class:animot-menu-visible={menuVisible}
	bind:this={containerEl}
	onmousemove={resetMouseIdleTimer}
	role="region"
	aria-label="Animot Presentation"
>
	{#if loading}
		<div class="animot-loading"><div class="animot-spinner"></div></div>
	{:else if error}
		<div class="animot-error">{error}</div>
	{:else if project && currentSlide}
		<div class="animot-canvas-wrapper" style:transform="scale({presentationScale})">
			<div
				class="animot-canvas {transitionClass}"
				class:forward={transitionDirection === 'forward'}
				class:backward={transitionDirection === 'backward'}
				style:width="{canvasWidth}px"
				style:height="{canvasHeight}px"
				style:--transition-duration="{transitionDurationMs}ms"
				style={backgroundStyle}
			>
				{#if currentSlide.canvas.background.particles?.enabled}
					<ParticlesBackground config={currentSlide.canvas.background.particles} width={canvasWidth} height={canvasHeight} />
				{/if}
				{#if currentSlide.canvas.background.confetti?.enabled}
					<ConfettiEffect config={currentSlide.canvas.background.confetti} width={canvasWidth} height={canvasHeight} />
				{/if}

				{#each sortedElementIds as elementId}
					{@const element = elementContent.get(elementId)}
					{@const animated = animatedElements.get(elementId)}
					{@const floatCfg = element?.floatingAnimation}
					{@const hasFloat = floatCfg?.enabled}
					{@const floatGroupId = element?.groupId}
					{@const mpConfig = element?.motionPathConfig}
					{@const mpElement = mpConfig ? currentSlide?.canvas.elements.find(el => el.id === mpConfig.motionPathId) as MotionPathElement | undefined : undefined}
					{@const mpProgress = animated?.motionPathProgress?.current ?? 0}
					{@const mpPoint = mpElement && mpConfig ? getPresenterPointOnPath(mpElement.points, mpElement.closed, (mpConfig.startPercent + (mpConfig.endPercent - mpConfig.startPercent) * mpProgress) / 100) : null}
					{@const elemX = mpPoint ? mpPoint.x - (animated?.width.current ?? 0) / 2 : animated?.x.current ?? 0}
					{@const elemY = mpPoint ? mpPoint.y - (animated?.height.current ?? 0) / 2 : animated?.y.current ?? 0}
					{@const mpRotation = mpPoint && mpConfig?.autoRotate
						? mpPoint.angle + (mpConfig.orientationOffset ?? 0)
						: null}
					{#if element && animated && animated.opacity.current > 0.01 && element.visible !== false && !(element.type === 'motionPath' && !(element as MotionPathElement).showInPresentation)}
						<div
							class="animot-element"
							class:floating={hasFloat}
							style:left="{elemX}px"
							style:top="{elemY}px"
							style:width="{animated.width.current}px"
							style:height="{animated.height.current}px"
							style:opacity={animated.opacity.current}
							style:transform="perspective({animated.perspective.current}px) rotateX({animated.tiltX.current}deg) rotateY({animated.tiltY.current}deg) rotate({mpRotation ?? animated.rotation.current}deg) skewX({animated.skewX.current}deg) skewY({animated.skewY.current}deg)"
							style:transform-origin={element.tiltOrigin ?? 'center'}
							style:backface-visibility={element.backfaceVisibility ?? 'visible'}
							style:z-index={element.zIndex}
							style:--float-amp="{hasFloat ? computeFloatAmp(floatCfg, floatGroupId || elementId) : 10}px"
							style:--float-speed="{hasFloat ? computeFloatSpeed(floatCfg, floatGroupId || elementId) : 3}s"
							style:--float-delay="{hashFraction(floatGroupId || elementId, 3) * 2}s"
							style:animation-name={hasFloat ? getFloatAnimName(floatCfg!.direction, floatGroupId || elementId) : 'none'}
							style:filter={(() => { const parts: string[] = []; const b = animated.blur.current; const br2 = animated.brightness.current; const c = animated.contrast.current; const s = animated.saturate.current; const g = animated.grayscale.current; if (b) parts.push(`blur(${b}px)`); if (br2 !== 100) parts.push(`brightness(${br2}%)`); if (c !== 100) parts.push(`contrast(${c}%)`); if (s !== 100) parts.push(`saturate(${s}%)`); if (g) parts.push(`grayscale(${g}%)`); return parts.length ? parts.join(' ') : 'none'; })()}
						>
							{#if element.type === 'code'}
								{@const codeEl = element as CodeElement}
								{@const morphState = codeMorphState.get(codeEl.id)}
								<div class="animot-code-block" class:transparent-bg={codeEl.transparentBackground} style:font-size="{codeEl.fontSize}px" style:font-weight={codeEl.fontWeight || 400} style:padding="{codeEl.padding}px" style:border-radius="{animated.borderRadius.current}px" style:background={codeEl.bgColor ?? '#0d1117'}>
									{#if codeEl.showHeader}
										<div class="animot-code-header" class:macos={codeEl.headerStyle === 'macos'} class:windows={codeEl.headerStyle === 'windows'} style:border-radius="{codeEl.headerRadius ?? animated.borderRadius.current}px {codeEl.headerRadius ?? animated.borderRadius.current}px 0 0">
											{#if codeEl.headerStyle === 'macos'}
												<div class="animot-window-controls">
													<span class="animot-control close"></span>
													<span class="animot-control minimize"></span>
													<span class="animot-control maximize"></span>
												</div>
											{:else if codeEl.headerStyle === 'windows'}
												<div class="animot-window-controls">
													<span class="animot-control win-minimize">
														<svg width="10" height="10" viewBox="0 0 10 10"><path d="M2 5h6" stroke="currentColor" stroke-width="1.2"/></svg>
													</span>
													<span class="animot-control win-maximize">
														<svg width="10" height="10" viewBox="0 0 10 10"><rect x="1.5" y="1.5" width="7" height="7" rx="0.5" fill="none" stroke="currentColor" stroke-width="1.2"/></svg>
													</span>
													<span class="animot-control win-close">
														<svg width="10" height="10" viewBox="0 0 10 10"><path d="M2 2l6 6M8 2l-6 6" stroke="currentColor" stroke-width="1.2"/></svg>
													</span>
												</div>
											{/if}
											<div class="animot-filename-tab" style:border-radius="{codeEl.tabRadius ?? 6}px">
												<svg class="animot-file-icon" width="14" height="14" viewBox="0 0 16 16" fill="none">
													<path d="M4 1h5.5L13 4.5V14a1 1 0 01-1 1H4a1 1 0 01-1-1V2a1 1 0 011-1z" stroke="currentColor" stroke-width="1.2" opacity="0.5"/>
													<path d="M9.5 1v3.5H13" stroke="currentColor" stroke-width="1.2" opacity="0.5"/>
												</svg>
												<span class="animot-filename">{codeEl.filename}</span>
											</div>
											<button class="animot-copy-code-btn" onclick={(e) => { e.stopPropagation(); navigator.clipboard.writeText(codeEl.code); const btn = e.currentTarget as HTMLElement; btn.classList.add('copied'); setTimeout(() => btn.classList.remove('copied'), 1500); }}>
												<span class="animot-copy-label">Copy</span><span class="animot-copied-label">Copied!</span>
												<svg class="animot-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
												<svg class="animot-check-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>
											</button>
										</div>
									{:else}
										<button class="animot-copy-code-btn animot-floating" onclick={(e) => { e.stopPropagation(); navigator.clipboard.writeText(codeEl.code); const btn = e.currentTarget as HTMLElement; btn.classList.add('copied'); setTimeout(() => btn.classList.remove('copied'), 1500); }}>
											<span class="animot-copy-label">Copy</span><span class="animot-copied-label">Copied!</span>
											<svg class="animot-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
											<svg class="animot-check-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>
										</button>
									{/if}
									<div class="animot-code-content">
										<div class="animot-highlighted-code">
											{#if morphState && morphState.oldCode !== morphState.newCode && morphState.mode !== 'instant'}
												{#key currentSlideIndex}
													<CodeMorph oldCode={morphState?.oldCode ?? ''} newCode={morphState?.newCode ?? ''} language={codeEl.language} theme={codeEl.theme} mode={morphState?.mode ?? 'highlight-changes'} speed={morphState?.speed ?? 50} highlightColor={morphState?.highlightColor ?? '#fef08a'} highlightDuration={codeEl.animation?.highlightDuration || 1000} showLineNumbers={(getElementInSlide(currentSlide, codeEl.id) as CodeElement | undefined)?.showLineNumbers ?? false} />
												{/key}
											{:else}
												{@html getCodeHighlight(codeEl.id)}
											{/if}
										</div>
									</div>
								</div>
							{:else if element.type === 'text'}
								{@const textEl = element as TextElement}
								{@const animFontSize = animated.fontSize?.current ?? textEl.fontSize}
								{@const typewriterState = textTypewriterState.get(element.id)}
								{@const displayText = typewriterState?.isAnimating ? typewriterState.fullText.slice(0, typewriterState.displayedChars) : textEl.content}
								<div
									class="animot-text-element"
									style:font-size="{animFontSize}px"
									style:font-weight={textEl.fontWeight}
									style:font-family="'{textEl.fontFamily}', sans-serif"
									style:font-style={textEl.fontStyle ?? 'normal'}
									style:text-decoration={textEl.textDecoration ?? 'none'}
									style:color={textEl.backgroundImage ? 'transparent' : (textEl.hollow && textEl.textStroke?.enabled ? 'transparent' : textEl.color)}
									style:background-color={textEl.backgroundImage ? 'transparent' : textEl.backgroundColor}
									style:background-image={textEl.backgroundImage ? `url(${textEl.backgroundImage})` : 'none'}
									style:background-size={textEl.backgroundImage ? `${textEl.backgroundScale ?? 100}%` : 'cover'}
									style:background-position={textEl.backgroundImage ? `${textEl.backgroundPositionX ?? 50}% ${textEl.backgroundPositionY ?? 50}%` : 'center'}
									style:-webkit-background-clip={textEl.backgroundImage ? 'text' : 'border-box'}
									style:background-clip={textEl.backgroundImage ? 'text' : 'border-box'}
									style:padding="{textEl.padding}px"
									style:border-radius="{textEl.borderRadius}px"
									style:text-align={textEl.textAlign}
									style:justify-content={textEl.textAlign === 'center' ? 'center' : textEl.textAlign === 'right' ? 'flex-end' : 'flex-start'}
									style:opacity={textEl.opacity ?? 1}
									style:-webkit-text-stroke={textEl.textStroke?.enabled ? `${textEl.textStroke.width}px ${textEl.textStroke.color}` : '0'}
									style:text-shadow={textEl.textShadow?.enabled ? `${textEl.textShadow.offsetX}px ${textEl.textShadow.offsetY}px ${textEl.textShadow.blur}px ${textEl.textShadow.color}` : 'none'}
								>
									{displayText}{#if typewriterState?.isAnimating}<span class="animot-typewriter-cursor">|</span>{/if}
								</div>
							{:else if element.type === 'arrow'}
								{@const arrowEl = element as ArrowElement}
								{@const cp = arrowEl.controlPoints || []}
								{@const pathD = cp.length === 0 ? `M ${arrowEl.startPoint.x} ${arrowEl.startPoint.y} L ${arrowEl.endPoint.x} ${arrowEl.endPoint.y}` : cp.length === 1 ? `M ${arrowEl.startPoint.x} ${arrowEl.startPoint.y} Q ${cp[0].x} ${cp[0].y} ${arrowEl.endPoint.x} ${arrowEl.endPoint.y}` : cp.length === 2 ? `M ${arrowEl.startPoint.x} ${arrowEl.startPoint.y} C ${cp[0].x} ${cp[0].y} ${cp[1].x} ${cp[1].y} ${arrowEl.endPoint.x} ${arrowEl.endPoint.y}` : buildCatmullRomPath(arrowEl.startPoint, cp, arrowEl.endPoint)}
								{@const lastCp = cp.length > 0 ? cp[cp.length - 1] : arrowEl.startPoint}
								{@const endAngle = Math.atan2(arrowEl.endPoint.y - lastCp.y, arrowEl.endPoint.x - lastCp.x)}
								{@const headAngle = Math.PI / 6}
								{@const headSize = arrowEl.headSize}
								{@const arrowHeadPath = `M ${arrowEl.endPoint.x - headSize * Math.cos(endAngle - headAngle)} ${arrowEl.endPoint.y - headSize * Math.sin(endAngle - headAngle)} L ${arrowEl.endPoint.x} ${arrowEl.endPoint.y} L ${arrowEl.endPoint.x - headSize * Math.cos(endAngle + headAngle)} ${arrowEl.endPoint.y - headSize * Math.sin(endAngle + headAngle)}`}
								{@const arrowAnimMode = arrowEl.animation?.mode ?? 'none'}
								{@const arrowAnimDuration = arrowEl.animation?.duration ?? 500}
								{@const isStyledArrow = arrowEl.style !== 'solid'}
								{@const isDrawType = arrowAnimMode === 'draw' || arrowAnimMode === 'undraw' || arrowAnimMode === 'draw-undraw'}
								{@const baseDashArray = arrowEl.style === 'dashed' ? '10,5' : arrowEl.style === 'dotted' ? '2,5' : 'none'}
								<svg class="animot-arrow-element" class:arrow-animate-draw={arrowAnimMode === 'draw' && !isStyledArrow} class:arrow-animate-undraw={arrowAnimMode === 'undraw' && !isStyledArrow} class:arrow-animate-draw-undraw={arrowAnimMode === 'draw-undraw' && !isStyledArrow} class:arrow-animate-grow={arrowAnimMode === 'grow'} viewBox="0 0 {arrowEl.size.width} {arrowEl.size.height}" preserveAspectRatio="none" style="--arrow-anim-duration: {arrowAnimDuration}ms;">
									<path class="arrow-path" d={pathD} fill="none" stroke={arrowEl.color} stroke-width={arrowEl.strokeWidth} stroke-dasharray={baseDashArray} stroke-linecap="round" stroke-linejoin="round" use:animateStyledArrowDraw={{ enabled: isDrawType && isStyledArrow, mode: arrowAnimMode, duration: arrowAnimDuration, dashPattern: baseDashArray, startX: arrowEl.startPoint.x, endX: arrowEl.endPoint.x, slideIndex: currentSlideIndex }} />
									{#if arrowEl.showHead !== false}
										<path class="arrow-head" class:arrow-head-styled-draw={isDrawType && isStyledArrow} class:arrow-head-undraw={arrowAnimMode === 'undraw'} class:arrow-head-draw-undraw={arrowAnimMode === 'draw-undraw'} d={arrowHeadPath} fill="none" stroke={arrowEl.color} stroke-width={arrowEl.strokeWidth} stroke-linecap="round" stroke-linejoin="round" style={isDrawType && isStyledArrow ? `--arrow-anim-duration: ${arrowAnimDuration}ms;` : ''} />
									{/if}
								</svg>
							{:else if element.type === 'image'}
								{@const imgEl = element as ImageElement}
								{@const clipPath = imgEl.clipMask?.enabled ? (imgEl.clipMask.shapeType === 'circle' ? 'circle(50% at 50% 50%)' : imgEl.clipMask.shapeType === 'ellipse' ? 'ellipse(50% 50% at 50% 50%)' : imgEl.clipMask.shapeType === 'triangle' ? 'polygon(50% 0%, 0% 100%, 100% 100%)' : imgEl.clipMask.shapeType === 'star' ? 'polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%)' : imgEl.clipMask.shapeType === 'hexagon' ? 'polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%)' : (imgEl.clipMask.borderRadius ?? 0) > 0 ? `inset(0 round ${imgEl.clipMask.borderRadius}px)` : 'none') : 'none'}
								<img class="animot-image-element" src={imgEl.src} alt="" style:object-fit={imgEl.objectFit} style:border-radius="{imgEl.clipMask?.enabled ? 0 : imgEl.borderRadius}px" style:opacity={imgEl.opacity} style:clip-path={clipPath} style:background-color={imgEl.backgroundColor ?? 'transparent'} />
							{:else if element.type === 'shape'}
								{@const shapeEl = element as ShapeElement}
								{@const animFill = animated.fillColor?.current ?? shapeEl.fillColor}
								{@const animStroke = animated.strokeColor?.current ?? shapeEl.strokeColor}
								{@const animStrokeWidth = animated.strokeWidth?.current ?? shapeEl.strokeWidth}
								{@const mState = shapeMorphStates.get(element.id)}
								{@const morphProgress = animated.shapeMorph?.current ?? 1}
								{@const effectiveShapeType = mState ? (morphProgress >= 1 ? mState.toType : (morphProgress <= 0 ? mState.fromType : null)) : shapeEl.shapeType}
								{@const isMorphing = mState && morphProgress > 0 && morphProgress < 1}
								<svg class="animot-shape-element" viewBox="0 0 {animated.width.current} {animated.height.current}" style:opacity={shapeEl.opacity} style:filter={shapeEl.boxShadow?.enabled ? `drop-shadow(${shapeEl.boxShadow.offsetX}px ${shapeEl.boxShadow.offsetY}px ${shapeEl.boxShadow.blur}px ${shapeEl.boxShadow.color})` : 'none'}>
									{#if isMorphing}
										{@const w = animated.width.current}
										{@const h = animated.height.current}
										{@const sw = animStrokeWidth}
										<g style:opacity={1 - morphProgress}>{@html renderShape(mState!.fromType, w, h, animated.borderRadius.current, animFill, animStroke, sw)}</g>
										<g style:opacity={morphProgress}>{@html renderShape(mState!.toType, w, h, animated.borderRadius.current, animFill, animStroke, sw)}</g>
									{:else}
										{@html renderShape(effectiveShapeType ?? shapeEl.shapeType, animated.width.current, animated.height.current, animated.borderRadius.current, animFill, animStroke, animStrokeWidth)}
									{/if}
								</svg>
							{:else if element.type === 'counter'}
								<CounterRenderer element={element as CounterElement} slideId={currentSlide?.id ?? ''} />
							{:else if element.type === 'chart'}
								<ChartRenderer element={element as ChartElement} slideId={currentSlide?.id ?? ''} />
							{:else if element.type === 'icon'}
								<IconRenderer element={element as IconElement} />
							{:else if element.type === 'svg'}
								{@const svgEl = element as SvgElement}
								{@const svgParsed = (() => { const m = svgEl.svgContent.trim().match(/^<svg([^>]*)>([\s\S]*)<\/svg>$/i); if (m) { const vb = m[1].match(/viewBox=["']([^"']+)["']/i); return { inner: m[2], viewBox: vb ? vb[1] : null }; } return { inner: svgEl.svgContent, viewBox: null }; })()}
								<div class="animot-svg-element" style:opacity={svgEl.opacity}>
									<svg width="100%" height="100%" viewBox={svgEl.viewBox ?? svgParsed.viewBox ?? `0 0 ${svgEl.size.width} ${svgEl.size.height}`} preserveAspectRatio={svgEl.preserveAspectRatio} xmlns="http://www.w3.org/2000/svg">
										<g style={svgEl.color ? `fill:${svgEl.color};stroke:${svgEl.color}` : ''}>
											{@html svgParsed.inner}
										</g>
									</svg>
								</div>
							{:else if element.type === 'motionPath'}
								{@const mpEl = element as MotionPathElement}
								{#if mpEl.showInPresentation}
									<svg width="100%" height="100%" style="position:absolute;top:0;left:0;pointer-events:none;overflow:visible;">
										<path d={buildPresenterPathD(mpEl.points, mpEl.closed)} stroke={mpEl.pathColor} stroke-width={mpEl.pathWidth} fill="none" stroke-dasharray="8 4" />
									</svg>
								{/if}
							{/if}
						</div>
					{/if}
				{/each}
			</div>
		</div>

		{#if arrows}
			<button class="animot-arrow animot-arrow-left" onclick={() => animateToSlide(currentSlideIndex - 1)} disabled={currentSlideIndex === 0 || isTransitioning} aria-label="Previous slide">
				<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg>
			</button>
			<button class="animot-arrow animot-arrow-right" onclick={() => animateToSlide(currentSlideIndex + 1)} disabled={currentSlideIndex === slides.length - 1 || isTransitioning} aria-label="Next slide">
				<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>
			</button>
		{/if}

		{#if controls}
			<div class="animot-controls">
				<button onclick={() => animateToSlide(currentSlideIndex - 1)} disabled={currentSlideIndex === 0 || isTransitioning} aria-label="Previous">
					<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
				</button>
				<span class="animot-slide-indicator">{currentSlideIndex + 1} / {slides.length}</span>
				<button onclick={() => animateToSlide(currentSlideIndex + 1)} disabled={currentSlideIndex === slides.length - 1 || isTransitioning} aria-label="Next">
					<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
				</button>
				<button onclick={() => { isAutoplay = !isAutoplay; if (!isAutoplay) clearAutoplayTimer(); }} class:active={isAutoplay} aria-label={isAutoplay ? 'Pause' : 'Play'}>
					<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
						{#if isAutoplay}<rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/>{:else}<polygon points="5 3 19 12 5 21 5 3"/>{/if}
					</svg>
				</button>
			</div>
		{/if}

		{#if showProgress}
			<div class="animot-progress-bar">
				<div class="animot-progress-fill" style:width="{((currentSlideIndex + 1) / slides.length) * 100}%"></div>
			</div>
		{/if}
	{/if}
</div>

<script module lang="ts">
	function renderShape(type: string, w: number, h: number, br: number, fill: string, stroke: string, sw: number): string {
		switch (type) {
			case 'rectangle': return `<rect x="${sw/2}" y="${sw/2}" width="${w-sw}" height="${h-sw}" rx="${br}" ry="${br}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}"/>`;
			case 'circle': return `<circle cx="${w/2}" cy="${h/2}" r="${Math.min(w,h)/2-sw/2}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}"/>`;
			case 'ellipse': return `<ellipse cx="${w/2}" cy="${h/2}" rx="${w/2-sw/2}" ry="${h/2-sw/2}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}"/>`;
			case 'triangle': return `<polygon points="${w/2},${sw/2} ${sw/2},${h-sw/2} ${w-sw/2},${h-sw/2}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}" stroke-linejoin="round"/>`;
			case 'star': {
				const cx = w/2, cy = h/2, outerR = Math.min(w,h)/2-sw/2, innerR = outerR*0.4;
				const pts = Array.from({length:10},(_,i)=>{const a=(i*Math.PI/5)-Math.PI/2;const r=i%2===0?outerR:innerR;return`${cx+r*Math.cos(a)},${cy+r*Math.sin(a)}`;}).join(' ');
				return `<polygon points="${pts}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}" stroke-linejoin="round"/>`;
			}
			case 'hexagon': {
				const cx = w/2, cy = h/2, r = Math.min(w,h)/2-sw/2;
				const pts = Array.from({length:6},(_,i)=>{const a=(i*Math.PI/3)-Math.PI/2;return`${cx+r*Math.cos(a)},${cy+r*Math.sin(a)}`;}).join(' ');
				return `<polygon points="${pts}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}" stroke-linejoin="round"/>`;
			}
			default: return '';
		}
	}
</script>

<style>
	/* Universal reset — mirrors the animot app's global * reset to prevent
	   host page defaults (margins on p/h1, padding, box-sizing) from leaking in */
	.animot-presenter :global(*) {
		margin: 0;
		padding: 0;
		box-sizing: border-box;
	}

	.animot-presenter {
		position: relative;
		width: 100%;
		height: 100%;
		display: flex;
		align-items: center;
		justify-content: center;
		overflow: hidden;
		background: transparent;
		/* Reset inheritable CSS from host page to prevent style leakage */
		line-height: normal;
		font-size: 16px;
		font-weight: 400;
		font-style: normal;
		letter-spacing: normal;
		word-spacing: normal;
		text-transform: none;
		text-indent: 0;
		text-align: left;
		color: inherit;
	}

	.animot-canvas-wrapper {
		display: flex;
		align-items: center;
		justify-content: center;
	}

	.animot-canvas {
		position: relative;
		overflow: hidden;
	}

	.animot-element {
		position: absolute;
		box-sizing: border-box;
		will-change: transform, opacity, left, top, width, height;
		isolation: isolate;
	}

	.animot-element.floating {
		animation-duration: var(--float-speed, 3s);
		animation-timing-function: ease-in-out;
		animation-iteration-count: infinite;
		animation-delay: var(--float-delay, 0s);
	}

	/* Code */
	.animot-code-block {
		width: 100%; height: 100%; overflow: hidden;
		display: flex; flex-direction: column; box-shadow: 0 8px 32px rgba(0,0,0,0.4);
		margin: 0; box-sizing: border-box;
	}
	.animot-code-block.transparent-bg { background: transparent !important; box-shadow: none; }
	.animot-code-block.transparent-bg .animot-code-header { background: transparent; border-bottom-color: rgba(255,255,255,0.06); }
	.animot-code-header { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: rgba(0, 0, 0, 0.2); border-bottom: 1px solid rgba(255, 255, 255, 0.06); flex-shrink: 0; min-height: 40px; }
	.animot-window-controls { display: flex; gap: 8px; align-items: center; flex-shrink: 0; }
	.macos .animot-control { width: 12px; height: 12px; border-radius: 50%; display: block; }
	.macos .animot-control.close { background: #ff5f57; }
	.macos .animot-control.minimize { background: #febc2e; }
	.macos .animot-control.maximize { background: #28c840; }
	.windows .animot-window-controls { order: 99; margin-left: auto; gap: 0; }
	.windows .animot-control { display: flex; align-items: center; justify-content: center; width: 28px; height: 24px; border-radius: 4px; color: rgba(255,255,255,0.45); }
	.animot-filename-tab { display: flex; align-items: center; gap: 6px; background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.08); border-radius: 6px; padding: 4px 10px; max-width: 220px; color: rgba(255,255,255,0.4); }
	.animot-file-icon { flex-shrink: 0; }
	.animot-filename { color: rgba(255,255,255,0.55); font-size: 12px; line-height: 18px; }
	.animot-copy-code-btn { display: flex; align-items: center; gap: 5px; height: 28px; padding: 0 8px; margin-left: auto; background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1); border-radius: 6px; color: rgba(255,255,255,0.4); cursor: pointer; opacity: 0; transition: opacity 0.2s, background 0.15s, color 0.15s; flex-shrink: 0; font-size: 12px; font-family: inherit; white-space: nowrap; }
	.animot-copy-code-btn:hover { background: rgba(255,255,255,0.12); color: rgba(255,255,255,0.8); }
	.animot-copy-code-btn svg { width: 14px; height: 14px; flex-shrink: 0; }
	.animot-copy-code-btn .animot-check-icon { display: none; }
	.animot-copy-code-btn .animot-copied-label { display: none; }
	.animot-copy-code-btn.copied .animot-copy-icon { display: none; }
	.animot-copy-code-btn.copied .animot-copy-label { display: none; }
	.animot-copy-code-btn.copied .animot-check-icon { display: block; color: #4ade80; }
	.animot-copy-code-btn.copied .animot-copied-label { display: inline; color: #4ade80; }
	.animot-copy-code-btn.animot-floating { position: absolute; top: 8px; right: 8px; z-index: 2; }
	.animot-code-block:hover .animot-copy-code-btn { opacity: 1; }
	.animot-code-content { flex: 1; overflow: hidden; position: relative; }
	.animot-highlighted-code { width: 100%; height: 100%; }
	.animot-code-content :global(pre), .animot-highlighted-code :global(pre) { margin: 0; padding: 16px; background: transparent !important; line-height: 1.6; font-size: inherit; overflow: visible; }
	.animot-highlighted-code :global(code) { font-family: inherit; font-size: inherit; font-weight: inherit; }
	.animot-highlighted-code :global(.line-number) { display: inline-block; width: 2.5em; margin-right: 1em; text-align: right; color: #6e7681; user-select: none; opacity: 0.6; }

	/* Text */
	.animot-text-element { width: 100%; height: 100%; display: flex; align-items: center; white-space: pre-wrap; word-wrap: break-word; }
	.animot-typewriter-cursor { animation: animot-blink 0.7s infinite; font-weight: 100; }
	@keyframes animot-blink { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0; } }

	/* Arrow */
	.animot-arrow-element { width: 100%; height: 100%; }
	.arrow-animate-draw .arrow-path { stroke-dasharray: 1000; stroke-dashoffset: 1000; animation: animot-arrow-draw var(--arrow-anim-duration, 500ms) ease-out forwards; }
	.arrow-animate-undraw .arrow-path { stroke-dasharray: 1000; stroke-dashoffset: 0; animation: animot-arrow-undraw var(--arrow-anim-duration, 500ms) ease-out forwards; }
	.arrow-animate-draw-undraw .arrow-path { stroke-dasharray: 1000; stroke-dashoffset: 1000; animation: animot-arrow-draw-undraw var(--arrow-anim-duration, 500ms) ease-out forwards; }
	.arrow-head-styled-draw { opacity: 0; animation: animot-arrow-head-appear var(--arrow-anim-duration, 500ms) ease-out forwards; animation-delay: calc(var(--arrow-anim-duration, 500ms) * 0.7); }
	.arrow-animate-draw .arrow-head { opacity: 0; animation: animot-arrow-head-appear var(--arrow-anim-duration, 500ms) ease-out forwards; animation-delay: calc(var(--arrow-anim-duration, 500ms) * 0.7); }
	.arrow-animate-undraw .arrow-head, .arrow-head-undraw { opacity: 1; animation: animot-arrow-head-disappear var(--arrow-anim-duration, 500ms) ease-out forwards; }
	.arrow-animate-draw-undraw .arrow-head, .arrow-head-draw-undraw { opacity: 0; animation: animot-arrow-head-draw-undraw var(--arrow-anim-duration, 500ms) ease-out forwards; }
	.arrow-animate-grow { transform-origin: left center; animation: animot-arrow-grow var(--arrow-anim-duration, 500ms) ease-out forwards; }
	@keyframes animot-arrow-draw { to { stroke-dashoffset: 0; } }
	@keyframes animot-arrow-undraw { from { stroke-dashoffset: 0; } to { stroke-dashoffset: 1000; } }
	@keyframes animot-arrow-draw-undraw { 0% { stroke-dashoffset: 1000; } 50% { stroke-dashoffset: 0; } 100% { stroke-dashoffset: 1000; } }
	@keyframes animot-arrow-head-appear { from { opacity: 0; } to { opacity: 1; } }
	@keyframes animot-arrow-head-disappear { 0% { opacity: 1; } 70% { opacity: 1; } 100% { opacity: 0; } }
	@keyframes animot-arrow-head-draw-undraw { 0% { opacity: 0; } 35% { opacity: 1; } 65% { opacity: 1; } 100% { opacity: 0; } }
	@keyframes animot-arrow-grow { from { transform: scaleX(0); opacity: 0; } to { transform: scaleX(1); opacity: 1; } }

	/* Image */
	.animot-image-element { width: 100%; height: 100%; display: block; }

	/* Shape */
	.animot-shape-element { width: 100%; height: 100%; display: block; overflow: visible; }

	/* Transitions */
	.animot-canvas { --transition-duration: 500ms; transition: transform calc(var(--transition-duration) * 0.4) ease, opacity calc(var(--transition-duration) * 0.4) ease; }
	.animot-canvas.transition-fade-out { opacity: 0; }
	.animot-canvas.transition-fade-in { animation: animot-fadeIn calc(var(--transition-duration) * 0.6) ease forwards; }
	.animot-canvas.transition-slide-left-out.forward { transform: translateX(-100%); opacity: 0; }
	.animot-canvas.transition-slide-left-in.forward { animation: animot-slideInFromRight calc(var(--transition-duration) * 0.6) ease forwards; }
	.animot-canvas.transition-slide-left-out.backward { transform: translateX(100%); opacity: 0; }
	.animot-canvas.transition-slide-left-in.backward { animation: animot-slideInFromLeft calc(var(--transition-duration) * 0.6) ease forwards; }
	.animot-canvas.transition-slide-right-out.forward { transform: translateX(100%); opacity: 0; }
	.animot-canvas.transition-slide-right-in.forward { animation: animot-slideInFromLeft calc(var(--transition-duration) * 0.6) ease forwards; }
	.animot-canvas.transition-slide-up-out { transform: translateY(-100%); opacity: 0; }
	.animot-canvas.transition-slide-up-in { animation: animot-slideInFromBottom calc(var(--transition-duration) * 0.6) ease forwards; }
	.animot-canvas.transition-slide-down-out { transform: translateY(100%); opacity: 0; }
	.animot-canvas.transition-slide-down-in { animation: animot-slideInFromTop calc(var(--transition-duration) * 0.6) ease forwards; }
	.animot-canvas.transition-zoom-in-out { transform: scale(0.5); opacity: 0; }
	.animot-canvas.transition-zoom-in-in { animation: animot-zoomIn calc(var(--transition-duration) * 0.6) ease forwards; }
	.animot-canvas.transition-zoom-out-out { transform: scale(1.5); opacity: 0; }
	.animot-canvas.transition-zoom-out-in { animation: animot-zoomOut calc(var(--transition-duration) * 0.6) ease forwards; }
	.animot-canvas.transition-flip-out { transform: perspective(1000px) rotateY(90deg); opacity: 0; }
	.animot-canvas.transition-flip-in { animation: animot-flipIn calc(var(--transition-duration) * 0.6) ease forwards; }

	@keyframes animot-fadeIn { from { opacity: 0; } to { opacity: 1; } }
	@keyframes animot-slideInFromRight { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
	@keyframes animot-slideInFromLeft { from { transform: translateX(-100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
	@keyframes animot-slideInFromBottom { from { transform: translateY(100%); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
	@keyframes animot-slideInFromTop { from { transform: translateY(-100%); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
	@keyframes animot-zoomIn { from { transform: scale(0.5); opacity: 0; } to { transform: scale(1); opacity: 1; } }
	@keyframes animot-zoomOut { from { transform: scale(1.5); opacity: 0; } to { transform: scale(1); opacity: 1; } }
	@keyframes animot-flipIn { from { transform: perspective(1000px) rotateY(-90deg); opacity: 0; } to { transform: perspective(1000px) rotateY(0); opacity: 1; } }

	/* Flip-X transition */
	.animot-canvas.transition-flip-x-out { transform: perspective(1000px) rotateX(90deg); opacity: 0; }
	.animot-canvas.transition-flip-x-in { animation: animot-flipXIn calc(var(--transition-duration) * 0.6) ease forwards; }
	@keyframes animot-flipXIn { from { transform: perspective(1000px) rotateX(-90deg); opacity: 0; } to { transform: perspective(1000px) rotateX(0); opacity: 1; } }

	/* Flip-Y transition */
	.animot-canvas.transition-flip-y-out { transform: perspective(1000px) rotateY(90deg); opacity: 0; }
	.animot-canvas.transition-flip-y-in { animation: animot-flipYIn calc(var(--transition-duration) * 0.6) ease forwards; }
	@keyframes animot-flipYIn { from { transform: perspective(1000px) rotateY(-90deg); opacity: 0; } to { transform: perspective(1000px) rotateY(0); opacity: 1; } }

	/* SVG element */
	.animot-svg-element { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }
	.animot-svg-element :global(svg) { width: 100%; height: 100%; }

	/* Controls */
	.animot-controls {
		position: absolute; bottom: 12px; left: 50%; transform: translateX(-50%);
		display: flex; align-items: center; gap: 8px; padding: 8px 16px;
		background: rgba(0,0,0,0.7); backdrop-filter: blur(10px); border-radius: 10px;
		opacity: 0; transition: opacity 0.3s ease 0.15s; z-index: 100;
	}
	.animot-presenter:hover .animot-controls, .animot-menu-visible .animot-controls { opacity: 1; transition-delay: 0s; }
	.animot-controls button {
		display: flex; align-items: center; justify-content: center;
		width: 32px; height: 32px; border-radius: 6px; border: none; cursor: pointer;
		background: rgba(255,255,255,0.1); color: white; transition: background 0.2s;
	}
	.animot-controls button:hover:not(:disabled) { background: rgba(255,255,255,0.2); }
	.animot-controls button:disabled { opacity: 0.3; cursor: not-allowed; }
	.animot-controls button.active { background: rgba(99,102,241,0.6); }
	.animot-controls button svg { width: 16px; height: 16px; }
	.animot-slide-indicator { font-size: 12px; color: white; min-width: 50px; text-align: center; font-family: system-ui, sans-serif; }

	/* Arrows */
	.animot-arrow {
		position: absolute; top: 50%; transform: translateY(-50%);
		width: 40px; height: 40px; border-radius: 50%; border: none; cursor: pointer;
		background: rgba(0,0,0,0.5); color: white; display: flex; align-items: center; justify-content: center;
		opacity: 0; transition: opacity 0.3s 0.15s; z-index: 100;
		/* Extra padding extends the hover hit area beyond the visible button */
		padding: 0; margin: 0;
	}
	.animot-presenter:hover .animot-arrow { opacity: 1; transition-delay: 0s; }
	.animot-presenter:hover .animot-arrow:disabled { opacity: 0.3; cursor: not-allowed; }
	.animot-arrow:hover:not(:disabled) { background: rgba(0,0,0,0.7); }
	.animot-arrow svg { width: 20px; height: 20px; }
	.animot-arrow-left { left: 8px; }
	.animot-arrow-right { right: 8px; }

	/* Progress bar */
	.animot-progress-bar {
		position: absolute; bottom: 0; left: 0; right: 0; height: 3px;
		background: rgba(255,255,255,0.1); z-index: 100;
		opacity: 0; transition: opacity 0.3s 0.15s;
	}
	.animot-presenter:hover .animot-progress-bar { opacity: 1; transition-delay: 0s; }
	.animot-progress-fill { height: 100%; background: linear-gradient(135deg, #7c3aed, #ec4899); transition: width 0.6s ease; }

	/* Loading / Error */
	.animot-loading { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; }
	.animot-spinner { width: 32px; height: 32px; border: 3px solid rgba(255,255,255,0.2); border-top-color: #7c3aed; border-radius: 50%; animation: animot-spin 0.8s linear infinite; }
	@keyframes animot-spin { to { transform: rotate(360deg); } }
	.animot-error { color: #ef4444; padding: 20px; text-align: center; font-family: system-ui, sans-serif; }
</style>
