Home / Articles / Blog

Animated Gradient Generator app
Gradient GeneratorCSSAnimationToolNextjs

Animated Gradient Generator app

How and Why I build one

Shrey Vijayvargiya

Hello and welcome to new blog

Today's story is about building an Animated Gradient Generator. Well, the idea came into mind because of gettemplate.website premium-templates, most premium-templates for our PRO clients of gettemplate need catchy and animated websites, including animated backgrounds

And every time I've to provide the same prompt to the cursor AI agent or probably use other websites and that's where I thought why can't I make one and release it for others to use.

Check demo: https://www.gettemplate.website/tool/gradient-generator

I usually make tools to automate my own work, and if I find a good tool, I launch it, giving a good UI.

A few examples can be found on iHateReading jobs portals, Universo, github trending repositories and roadmap templates. These are the features or products that I needed and then launched for others to use.

This one trick is my go-to approach for any new products. If I want and can make something good, I can launch it for the people as well.

This gradient generator provides background CSS that can be directly copied and pasted for use, and it is also available for download for other purposes. Since it includes animation, I've used ffmpeg to create videos in MP4 and GIF formats for people to download.

I do need this tool to create some background animations for YouTube videos quickly, banner images as well, and the reason I made it is that I am done opening figma and canva and logging in every time just for a small gradient generator.

These apps are good, but sometimes we need small apps that load quickly and work well for small or the smallest tasks. This is another reason why I am not building this app further or adding any more features; its whole purpose is to provide background gradients, nothing else.

Let's begin with how I made this tool

It's simple because I've experience, that's why I can say that, but it is simple in general in 2025 with the use of AI.

I am a big fan of the code file component as a product. What it means is, I write code in one single file to create the entire MVP of the product and avoid reuse and any other concepts. One single file is the MVP, and in general, I stick to the plan of writing almost 5k lines of code for every MVP

Our gradient generator, as shown below, has 3k lines of code

text
import React, {
	useState,
	useRef,
	useCallback,
	useEffect,
	useMemo,
} from "react";
import {
	Copy,
	Plus,
	Trash2,
	X,
	ChevronDown,
	Download,
	InfoIcon,
} from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";

// Gradient Presets - 20 presets with 2 color stops each
const gradientPresets = [
	{
		id: 1,
		name: "Sunset",
		type: "linear",
		angle: 45,
		stops: [
			{ id: 1, color: "#FF6B6B", position: { x: 0, y: 0 } },
			{ id: 2, color: "#FFE66D", position: { x: 100, y: 100 } },
		],
	},
	{
		id: 2,
		name: "Ocean",
		type: "linear",
		angle: 135,
		stops: [
			{ id: 1, color: "#4ECDC4", position: { x: 0, y: 0 } },
			{ id: 2, color: "#44A08D", position: { x: 100, y: 100 } },
		],
	},
	{
		id: 3,
		name: "Purple Dream",
		type: "linear",
		angle: 90,
		stops: [
			{ id: 1, color: "#667EEA", position: { x: 0, y: 0 } },
			{ id: 2, color: "#764BA2", position: { x: 100, y: 100 } },
		],
	},
	{
		id: 4,
		name: "Forest",
		type: "linear",
		angle: 45,
		stops: [
			{ id: 1, color: "#11998E", position: { x: 0, y: 0 } },
			{ id: 2, color: "#38EF7D", position: { x: 100, y: 100 } },
		],
	},
	{
		id: 5,
		name: "Coral",
		type: "linear",
		angle: 120,
		stops: [
			{ id: 1, color: "#FF9A9E", position: { x: 0, y: 0 } },
			{ id: 2, color: "#FECFEF", position: { x: 100, y: 100 } },
		],
	},
	{
		id: 6,
		name: "Blue Sky",
		type: "linear",
		angle: 180,
		stops: [
			{ id: 1, color: "#3494E6", position: { x: 0, y: 0 } },
			{ id: 2, color: "#EC6EAD", position: { x: 100, y: 100 } },
		],
	},
	{
		id: 7,
		name: "Peach",
		type: "linear",
		angle: 45,
		stops: [
			{ id: 1, color: "#FFECD2", position: { x: 0, y: 0 } },
			{ id: 2, color: "#FCB69F", position: { x: 100, y: 100 } },
		],
	},
	{
		id: 8,
		name: "Midnight",
		type: "linear",
		angle: 135,
		stops: [
			{ id: 1, color: "#0F2027", position: { x: 0, y: 0 } },
			{ id: 2, color: "#203A43", position: { x: 100, y: 100 } },
		],
	},
	{
		id: 9,
		name: "Rose Gold",
		type: "linear",
		angle: 90,
		stops: [
			{ id: 1, color: "#F093FB", position: { x: 0, y: 0 } },
			{ id: 2, color: "#F5576C", position: { x: 100, y: 100 } },
		],
	},
	{
		id: 10,
		name: "Mint",
		type: "linear",
		angle: 45,
		stops: [
			{ id: 1, color: "#A8EDEA", position: { x: 0, y: 0 } },
			{ id: 2, color: "#FED6E3", position: { x: 100, y: 100 } },
		],
	},
	{
		id: 11,
		name: "Fire",
		type: "linear",
		angle: 45,
		stops: [
			{ id: 1, color: "#FF416C", position: { x: 0, y: 0 } },
			{ id: 2, color: "#FF4B2B", position: { x: 100, y: 100 } },
		],
	},
	{
		id: 12,
		name: "Lavender",
		type: "linear",
		angle: 135,
		stops: [
			{ id: 1, color: "#E0C3FC", position: { x: 0, y: 0 } },
			{ id: 2, color: "#8EC5FC", position: { x: 100, y: 100 } },
		],
	},
	{
		id: 13,
		name: "zinc Tea",
		type: "linear",
		angle: 90,
		stops: [
			{ id: 1, color: "#D4FC79", position: { x: 0, y: 0 } },
			{ id: 2, color: "#96E6A1", position: { x: 100, y: 100 } },
		],
	},
	{
		id: 14,
		name: "Cherry",
		type: "linear",
		angle: 45,
		stops: [
			{ id: 1, color: "#EB3349", position: { x: 0, y: 0 } },
			{ id: 2, color: "#F45C43", position: { x: 100, y: 100 } },
		],
	},
	{
		id: 15,
		name: "Sky Blue",
		type: "linear",
		angle: 180,
		stops: [
			{ id: 1, color: "#89F7FE", position: { x: 0, y: 0 } },
			{ id: 2, color: "#66A6FF", position: { x: 100, y: 100 } },
		],
	},
	{
		id: 16,
		name: "Orange",
		type: "linear",
		angle: 120,
		stops: [
			{ id: 1, color: "#FFB347", position: { x: 0, y: 0 } },
			{ id: 2, color: "#FFCC33", position: { x: 100, y: 100 } },
		],
	},
	{
		id: 17,
		name: "Violet",
		type: "linear",
		angle: 135,
		stops: [
			{ id: 1, color: "#8360C3", position: { x: 0, y: 0 } },
			{ id: 2, color: "#2EBF91", position: { x: 100, y: 100 } },
		],
	},
	{
		id: 18,
		name: "Pink",
		type: "linear",
		angle: 45,
		stops: [
			{ id: 1, color: "#FF6B95", position: { x: 0, y: 0 } },
			{ id: 2, color: "#FFC796", position: { x: 100, y: 100 } },
		],
	},
	{
		id: 19,
		name: "Cyan",
		type: "linear",
		angle: 90,
		stops: [
			{ id: 1, color: "#00F5FF", position: { x: 0, y: 0 } },
			{ id: 2, color: "#00D4FF", position: { x: 100, y: 100 } },
		],
	},
	{
		id: 20,
		name: "Golden",
		type: "linear",
		angle: 45,
		stops: [
			{ id: 1, color: "#F6D365", position: { x: 0, y: 0 } },
			{ id: 2, color: "#FDA085", position: { x: 100, y: 100 } },
		],
	},
	{
		id: 21,
		name: "Dark Night",
		type: "linear",
		angle: 135,
		stops: [
			{ id: 1, color: "#1a1a2e", position: { x: 0, y: 0 } },
			{ id: 2, color: "#16213e", position: { x: 100, y: 100 } },
		],
	},
	{
		id: 22,
		name: "Deep Purple",
		type: "linear",
		angle: 90,
		stops: [
			{ id: 1, color: "#2d1b69", position: { x: 0, y: 0 } },
			{ id: 2, color: "#11998e", position: { x: 100, y: 100 } },
		],
	},
	{
		id: 23,
		name: "Charcoal",
		type: "linear",
		angle: 45,
		stops: [
			{ id: 1, color: "#2c3e50", position: { x: 0, y: 0 } },
			{ id: 2, color: "#34495e", position: { x: 100, y: 100 } },
		],
	},
	{
		id: 24,
		name: "Dark Blue",
		type: "linear",
		angle: 180,
		stops: [
			{ id: 1, color: "#0c0c0c", position: { x: 0, y: 0 } },
			{ id: 2, color: "#1a237e", position: { x: 100, y: 100 } },
		],
	},
	{
		id: 25,
		name: "Dark Forest",
		type: "linear",
		angle: 135,
		stops: [
			{ id: 1, color: "#0f2027", position: { x: 0, y: 0 } },
			{ id: 2, color: "#203a43", position: { x: 100, y: 100 } },
		],
	},
	{
		id: 26,
		name: "Ebony",
		type: "linear",
		angle: 90,
		stops: [
			{ id: 1, color: "#232526", position: { x: 0, y: 0 } },
			{ id: 2, color: "#414345", position: { x: 100, y: 100 } },
		],
	},
	{
		id: 27,
		name: "Dark Red",
		type: "linear",
		angle: 45,
		stops: [
			{ id: 1, color: "#1a0000", position: { x: 0, y: 0 } },
			{ id: 2, color: "#4a0000", position: { x: 100, y: 100 } },
		],
	},
	{
		id: 28,
		name: "Midnight Blue",
		type: "linear",
		angle: 120,
		stops: [
			{ id: 1, color: "#0f0c29", position: { x: 0, y: 0 } },
			{ id: 2, color: "#302b63", position: { x: 100, y: 100 } },
		],
	},
	{
		id: 29,
		name: "Dark Teal",
		type: "linear",
		angle: 135,
		stops: [
			{ id: 1, color: "#134e5e", position: { x: 0, y: 0 } },
			{ id: 2, color: "#71b280", position: { x: 100, y: 100 } },
		],
	},
	{
		id: 30,
		name: "Shadow",
		type: "linear",
		angle: 45,
		stops: [
			{ id: 1, color: "#1e3c72", position: { x: 0, y: 0 } },
			{ id: 2, color: "#2a5298", position: { x: 100, y: 100 } },
		],
	},
	{
		id: 31,
		name: "Dark Violet",
		type: "linear",
		angle: 90,
		stops: [
			{ id: 1, color: "#4b0082", position: { x: 0, y: 0 } },
			{ id: 2, color: "#6a0dad", position: { x: 100, y: 100 } },
		],
	},
	{
		id: 32,
		name: "Obsidian",
		type: "linear",
		angle: 180,
		stops: [
			{ id: 1, color: "#000000", position: { x: 0, y: 0 } },
			{ id: 2, color: "#1a1a1a", position: { x: 100, y: 100 } },
		],
	},
	{
		id: 33,
		name: "Dark Emerald",
		type: "linear",
		angle: 135,
		stops: [
			{ id: 1, color: "#0d2818", position: { x: 0, y: 0 } },
			{ id: 2, color: "#1a5f3f", position: { x: 100, y: 100 } },
		],
	},
	{
		id: 34,
		name: "Deep Space",
		type: "linear",
		angle: 45,
		stops: [
			{ id: 1, color: "#000000", position: { x: 0, y: 0 } },
			{ id: 2, color: "#1a1a2e", position: { x: 100, y: 100 } },
		],
	},
	{
		id: 35,
		name: "Dark Orange",
		type: "linear",
		angle: 120,
		stops: [
			{ id: 1, color: "#2c1810", position: { x: 0, y: 0 } },
			{ id: 2, color: "#8b4513", position: { x: 100, y: 100 } },
		],
	},
];

// Background Pattern Presets
const backgroundPatternPresets = [
	{
		id: 1,
		name: "None",
		css: "",
		preview: "transparent",
	},
	{
		id: 2,
		name: "Grid Squares",
		css: `linear-gradient(rgba(0, 0, 0, 0.1) 1px, transparent 1px),
			linear-gradient(90deg, rgba(0, 0, 0, 0.1) 1px, transparent 1px)`,
		backgroundSize: "20px 20px",
		preview: `linear-gradient(rgba(0, 0, 0, 0.1) 1px, transparent 1px),
			linear-gradient(90deg, rgba(0, 0, 0, 0.1) 1px, transparent 1px)`,
	},
	{
		id: 3,
		name: "Vertical Lines",
		css: `repeating-linear-gradient(90deg, rgba(0, 0, 0, 0.1) 0px, rgba(0, 0, 0, 0.1) 1px, transparent 1px, transparent 20px)`,
		backgroundSize: "20px 20px",
		preview: `repeating-linear-gradient(90deg, rgba(0, 0, 0, 0.1) 0px, rgba(0, 0, 0, 0.1) 1px, transparent 1px, transparent 20px)`,
	},
	{
		id: 4,
		name: "Horizontal Lines",
		css: `repeating-linear-gradient(0deg, rgba(0, 0, 0, 0.1) 0px, rgba(0, 0, 0, 0.1) 1px, transparent 1px, transparent 20px)`,
		backgroundSize: "20px 20px",
		preview: `repeating-linear-gradient(0deg, rgba(0, 0, 0, 0.1) 0px, rgba(0, 0, 0, 0.1) 1px, transparent 1px, transparent 20px)`,
	},
	{
		id: 5,
		name: "Diagonal Lines",
		css: `repeating-linear-gradient(45deg, rgba(0, 0, 0, 0.1) 0px, rgba(0, 0, 0, 0.1) 1px, transparent 1px, transparent 20px)`,
		backgroundSize: "20px 20px",
		preview: `repeating-linear-gradient(45deg, rgba(0, 0, 0, 0.1) 0px, rgba(0, 0, 0, 0.1) 1px, transparent 1px, transparent 20px)`,
	},
	{
		id: 6,
		name: "Dots Pattern",
		css: `radial-gradient(circle, rgba(0, 0, 0, 0.15) 1px, transparent 1px)`,
		backgroundSize: "20px 20px",
		preview: `radial-gradient(circle, rgba(0, 0, 0, 0.15) 1px, transparent 1px)`,
	},
	{
		id: 7,
		name: "Large Circle",
		css: `radial-gradient(circle at center, rgba(0, 0, 0, 0.1) 0%, transparent 50%)`,
		backgroundSize: "100% 100%",
		preview: `radial-gradient(circle at center, rgba(0, 0, 0, 0.1) 0%, transparent 50%)`,
	},
	{
		id: 8,
		name: "Ellipse Center",
		css: `radial-gradient(ellipse at center, rgba(0, 0, 0, 0.1) 0%, transparent 70%)`,
		backgroundSize: "100% 100%",
		preview: `radial-gradient(ellipse at center, rgba(0, 0, 0, 0.1) 0%, transparent 70%)`,
	},
	{
		id: 9,
		name: "Cross Pattern",
		css: `linear-gradient(rgba(0, 0, 0, 0.1) 1px, transparent 1px),
			linear-gradient(90deg, rgba(0, 0, 0, 0.1) 1px, transparent 1px)`,
		backgroundSize: "30px 30px",
		preview: `linear-gradient(rgba(0, 0, 0, 0.1) 1px, transparent 1px),
			linear-gradient(90deg, rgba(0, 0, 0, 0.1) 1px, transparent 1px)`,
	},
	{
		id: 10,
		name: "Hexagon Pattern",
		css: `repeating-linear-gradient(60deg, rgba(0, 0, 0, 0.1) 0px, rgba(0, 0, 0, 0.1) 1px, transparent 1px, transparent 30px),
			repeating-linear-gradient(120deg, rgba(0, 0, 0, 0.1) 0px, rgba(0, 0, 0, 0.1) 1px, transparent 1px, transparent 30px),
			repeating-linear-gradient(0deg, rgba(0, 0, 0, 0.1) 0px, rgba(0, 0, 0, 0.1) 1px, transparent 1px, transparent 30px)`,
		backgroundSize: "30px 30px",
		preview: `repeating-linear-gradient(60deg, rgba(0, 0, 0, 0.1) 0px, rgba(0, 0, 0, 0.1) 1px, transparent 1px, transparent 30px),
			repeating-linear-gradient(120deg, rgba(0, 0, 0, 0.1) 0px, rgba(0, 0, 0, 0.1) 1px, transparent 1px, transparent 30px),
			repeating-linear-gradient(0deg, rgba(0, 0, 0, 0.1) 0px, rgba(0, 0, 0, 0.1) 1px, transparent 1px, transparent 30px)`,
	},
	{
		id: 11,
		name: "Wavy Lines",
		css: `repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0, 0, 0, 0.1) 2px, rgba(0, 0, 0, 0.1) 4px)`,
		backgroundSize: "20px 20px",
		preview: `repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0, 0, 0, 0.1) 2px, rgba(0, 0, 0, 0.1) 4px)`,
	},
	{
		id: 12,
		name: "Checkerboard",
		css: `linear-gradient(45deg, rgba(0, 0, 0, 0.1) 25%, transparent 25%),
			linear-gradient(-45deg, rgba(0, 0, 0, 0.1) 25%, transparent 25%),
			linear-gradient(45deg, transparent 75%, rgba(0, 0, 0, 0.1) 75%),
			linear-gradient(-45deg, transparent 75%, rgba(0, 0, 0, 0.1) 75%)`,
		backgroundSize: "20px 20px",
		backgroundPosition: "0 0, 0 10px, 10px -10px, -10px 0px",
		preview: `linear-gradient(45deg, rgba(0, 0, 0, 0.1) 25%, transparent 25%),
			linear-gradient(-45deg, rgba(0, 0, 0, 0.1) 25%, transparent 25%),
			linear-gradient(45deg, transparent 75%, rgba(0, 0, 0, 0.1) 75%),
			linear-gradient(-45deg, transparent 75%, rgba(0, 0, 0, 0.1) 75%)`,
	},
];

// Helper function to generate gradient CSS from preset
const generatePresetGradientCSS = (preset) => {
	if (preset.type === "linear") {
		return `linear-gradient(${preset.angle || 45}deg, ${preset.stops[0].color}, ${preset.stops[1].color})`;
	} else if (preset.type === "radial") {
		return `radial-gradient(circle, ${preset.stops[0].color}, ${preset.stops[1].color})`;
	} else if (preset.type === "conic") {
		return `conic-gradient(${preset.stops[0].color}, ${preset.stops[1].color})`;
	}
	return `linear-gradient(45deg, ${preset.stops[0].color}, ${preset.stops[1].color})`;
};

// Custom Dropdown Component
const Dropdown = ({
	value,
	onChange,
	options,
	placeholder,
	className = "",
}) => {
	const [isOpen, setIsOpen] = useState(false);
	const dropdownRef = useRef(null);

	const handleClickOutside = useCallback((event) => {
		if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
			setIsOpen(false);
		}
	}, []);

	useEffect(() => {
		document.addEventListener("mousedown", handleClickOutside);
		return () => document.removeEventListener("mousedown", handleClickOutside);
	}, [handleClickOutside]);

	const selectedOption = options.find((option) => option.value === value);

	return (
		<div ref={dropdownRef} className={`relative ${className}`}>
			<button
				type="button"
				onClick={() => setIsOpen(!isOpen)}
				className="w-full h-8 px-2 text-xs border border-zinc-200 bg-white rounded-xl focus:outline-none focus:ring-2 focus:ring-zinc-400 focus:border-zinc-400 transition-colors flex items-center justify-between"
			>
				<span className="text-left">
					{selectedOption?.label || placeholder}
				</span>
				<ChevronDown
					className={`w-3 h-3 transition-transform ${
						isOpen ? "rotate-180" : ""
					}`}
				/>
			</button>

			<AnimatePresence>
				{isOpen && (
					<motion.div
						initial={{ opacity: 0, y: -10 }}
						animate={{ opacity: 1, y: 0 }}
						exit={{ opacity: 0, y: -10 }}
						transition={{ duration: 0.15 }}
						className="absolute z-50 w-full mt-1 bg-white border border-zinc-200 rounded-xl shadow-lg overflow-hidden"
					>
						{options.map((option) => (
							<button
								key={option.value}
								type="button"
								onClick={() => {
									onChange(option.value);
									setIsOpen(false);
								}}
								className={`w-full px-2 py-1.5 text-xs text-left hover:bg-zinc-50 transition-colors ${
									value === option.value
										? "bg-zinc-100 text-zinc-900"
										: "text-zinc-700"
								}`}
							>
								{option.label}
							</button>
						))}
					</motion.div>
				)}
			</AnimatePresence>
		</div>
	);
};

const GradientGenerator = () => {
	const [isModalOpen, setIsModalOpen] = useState(false);
	const [gradient, setGradient] = useState({
		type: "linear",
		angle: 45,
		stops: [
			{ id: 1, color: "#FF8D00", position: { x: 0, y: 0 } },
			{ id: 2, color: "#FFB870", position: { x: 50, y: 30 } },
		],
		noise: {
			enabled: true,
			intensity: 0.3,
		},
		animation: {
			enabled: true,
			type: "rotate",
			duration: 3,
			easing: "ease-in-out",
			direction: "normal",
		},
		backgroundAnimation: {
			enabled: true,
			type: "slide",
			direction: "right",
			speed: 5,
			easing: "linear",
		},
		dimensions: {
			width: 1080,
			height: 1920,
		},
		backgroundPattern: {
			id: 1,
			name: "None",
		},
	});

	const [isPlaying, setIsPlaying] = useState(true);
	const [copied, setCopied] = useState("");
	const [selectedStop, setSelectedStop] = useState(null);
	const [isDragging, setIsDragging] = useState(false);
	const [downloadDimension, setDownloadDimension] = useState("desktop");
	const [previewFrameSize, setPreviewFrameSize] = useState("desktop");
	const [isDownloadDropdownOpen, setIsDownloadDropdownOpen] = useState(false);
	const [isPanelDownloadDropdownOpen, setIsPanelDownloadDropdownOpen] =
		useState(false);
	const [isGeneratingMP4, setIsGeneratingMP4] = useState(false);
	const [mp4Progress, setMp4Progress] = useState(0);
	const [isGradientPresetsOpen, setIsGradientPresetsOpen] = useState(false);
	const [isBackgroundPatternsOpen, setIsBackgroundPatternsOpen] =
		useState(false);
	const gradientPresetsRef = useRef(null);
	const backgroundPatternsRef = useRef(null);
	const previewRef = useRef(null);
	const modalPreviewRef = useRef(null);
	const downloadDropdownRef = useRef(null);
	const panelDownloadDropdownRef = useRef(null);

	const dimensionPresets = {
		mobile: { width: 1080, height: 1920, label: "Mobile (1080×1920)" },
		desktop: { width: 1920, height: 1080, label: "Desktop (1920×1080)" },
	};

	const previewFramePresets = {
		mobile: { width: 1080, height: 1920, label: "Mobile", icon: "📱" },
		tablet: { width: 1024, height: 1366, label: "Tablet", icon: "📱" },
		desktop: { width: 1920, height: 1080, label: "Desktop", icon: "🖥️" },
		laptop: { width: 1366, height: 768, label: "Laptop", icon: "💻" },
		ultrawide: { width: 2560, height: 1080, label: "Ultrawide", icon: "🖥️" },
		custom: { width: 1080, height: 1080, label: "Square", icon: "⬜" },
	};

	const addColorStop = () => {
		const newStop = {
			id: Date.now(),
			color: "#10b981",
			position: {
				x: Math.random() * 80 + 10,
				y: Math.random() * 80 + 10,
			},
		};
		setGradient((prev) => ({
			...prev,
			stops: [...prev.stops, newStop],
		}));
	};

	const removeColorStop = (id) => {
		setGradient((prev) => ({
			...prev,
			stops: prev.stops.filter((stop) => stop.id !== id),
		}));
	};

	const updateColorStop = (id, property, value) => {
		setGradient((prev) => ({
			...prev,
			stops: prev.stops.map((stop) =>
				stop.id === id ? { ...stop, [property]: value } : stop
			),
		}));
	};

	const handleMouseDown = useCallback((e, stopId) => {
		e.preventDefault();
		setIsDragging(true);
		setSelectedStop(stopId);

		const previewRect = previewRef.current.getBoundingClientRect();

		const handleMouseMove = (e) => {
			const x = ((e.clientX - previewRect.left) / previewRect.width) * 100;
			const y = ((e.clientY - previewRect.top) / previewRect.height) * 100;

			const clampedX = Math.max(0, Math.min(100, x));
			const clampedY = Math.max(0, Math.min(100, y));

			updateColorStop(stopId, "position", { x: clampedX, y: clampedY });
		};

		const handleMouseUp = () => {
			setIsDragging(false);
			setSelectedStop(null);
			document.removeEventListener("mousemove", handleMouseMove);
			document.removeEventListener("mouseup", handleMouseUp);
		};

		document.addEventListener("mousemove", handleMouseMove);
		document.addEventListener("mouseup", handleMouseUp);
	}, []);

	const handleKeyDown = useCallback(
		(e, stopId) => {
			if (!selectedStop || selectedStop !== stopId) return;

			const step = e.shiftKey ? 10 : 1;
			const stop = gradient.stops.find((s) => s.id === stopId);
			if (!stop) return;

			let newPosition = { ...stop.position };

			switch (e.key) {
				case "ArrowLeft":
					newPosition.x = Math.max(0, stop.position.x - step);
					break;
				case "ArrowRight":
					newPosition.x = Math.min(100, stop.position.x + step);
					break;
				case "ArrowUp":
					newPosition.y = Math.max(0, stop.position.y - step);
					break;
				case "ArrowDown":
					newPosition.y = Math.min(100, stop.position.y + step);
					break;
				case "Delete":
				case "Backspace":
					removeColorStop(stopId);
					return;
				default:
					return;
			}

			e.preventDefault();
			updateColorStop(stopId, "position", newPosition);
		},
		[selectedStop, gradient.stops]
	);

	const generateGradientCSS = () => {
		// Handle empty case - return white background when no stops
		if (gradient.stops.length === 0) {
			return "#ffffff";
		}

		const sortedStops = [...gradient.stops].sort((a, b) => {
			const distA = Math.sqrt(
				a.position.x * a.position.x + a.position.y * a.position.y
			);
			const distB = Math.sqrt(
				b.position.x * b.position.x + b.position.y * b.position.y
			);
			return distA - distB;
		});

		if (gradient.type === "linear") {
			const firstStop = sortedStops[0];
			const lastStop = sortedStops[sortedStops.length - 1];
			const angle =
				Math.atan2(
					lastStop.position.y - firstStop.position.y,
					lastStop.position.x - firstStop.position.x
				) *
				(180 / Math.PI);

			const stopsString = sortedStops
				.map((stop) => `${stop.color} ${Math.round(stop.position.x)}%`)
				.join(", ");

			return `linear-gradient(${Math.round(angle)}deg, ${stopsString})`;
		}

		if (gradient.type === "radial") {
			const stopsString = sortedStops
				.map(
					(stop) =>
						`${stop.color} ${Math.round(
							Math.sqrt(
								Math.pow(stop.position.x - 50, 2) +
									Math.pow(stop.position.y - 50, 2)
							)
						)}%`
				)
				.join(", ");

			return `radial-gradient(circle at 50% 50%, ${stopsString})`;
		}

		if (gradient.type === "conic") {
			const stopsString = sortedStops
				.map((stop) => `${stop.color} ${Math.round(stop.position.x)}%`)
				.join(", ");

			return `conic-gradient(from 0deg, ${stopsString})`;
		}

		if (gradient.type === "mesh") {
			// Mesh gradient using multiple radial gradients as background layers
			const meshGradients = sortedStops
				.map(
					(stop) =>
						`radial-gradient(circle at ${stop.position.x}% ${stop.position.y}%, ${stop.color} 0%, transparent 50%)`
				)
				.join(", ");
			return meshGradients;
		}

		if (gradient.type === "grid") {
			// Grid pattern using repeating linear gradients
			const firstStop = sortedStops[0];
			const lastStop = sortedStops[sortedStops.length - 1];
			const angle =
				Math.atan2(
					lastStop.position.y - firstStop.position.y,
					lastStop.position.x - firstStop.position.x
				) *
				(180 / Math.PI);

			const stopsString = sortedStops
				.map((stop) => `${stop.color} ${Math.round(stop.position.x)}%`)
				.join(", ");

			return `repeating-linear-gradient(${Math.round(angle)}deg, ${stopsString})`;
		}

		if (gradient.type === "sharp") {
			// Sharp transitions - linear gradient with hard stops
			const firstStop = sortedStops[0];
			const lastStop = sortedStops[sortedStops.length - 1];
			const angle =
				Math.atan2(
					lastStop.position.y - firstStop.position.y,
					lastStop.position.x - firstStop.position.x
				) *
				(180 / Math.PI);

			// Create sharp transitions by duplicating colors at same position
			const sharpStops = sortedStops
				.flatMap((stop, index) => {
					if (index === sortedStops.length - 1) {
						return [`${stop.color} ${Math.round(stop.position.x)}%`];
					}
					const nextStop = sortedStops[index + 1];
					const midPoint = (stop.position.x + nextStop.position.x) / 2;
					return [
						`${stop.color} ${Math.round(stop.position.x)}%`,
						`${stop.color} ${Math.round(midPoint)}%`,
						`${nextStop.color} ${Math.round(midPoint)}%`,
					];
				})
				.join(", ");

			return `linear-gradient(${Math.round(angle)}deg, ${sharpStops})`;
		}

		if (gradient.type === "soft") {
			// Soft transitions - linear gradient with extended color stops
			const firstStop = sortedStops[0];
			const lastStop = sortedStops[sortedStops.length - 1];
			const angle =
				Math.atan2(
					lastStop.position.y - firstStop.position.y,
					lastStop.position.x - firstStop.position.x
				) *
				(180 / Math.PI);

			// Create soft transitions by extending color stops
			const softStops = sortedStops
				.map((stop, index) => {
					if (index === 0) {
						return `${stop.color} ${Math.max(0, Math.round(stop.position.x) - 10)}%`;
					}
					if (index === sortedStops.length - 1) {
						return `${stop.color} ${Math.min(100, Math.round(stop.position.x) + 10)}%`;
					}
					const prevStop = sortedStops[index - 1];
					const nextStop = sortedStops[index + 1];
					const startPos = Math.max(
						prevStop.position.x,
						Math.round(stop.position.x) - 15
					);
					const endPos = Math.min(
						nextStop.position.x,
						Math.round(stop.position.x) + 15
					);
					return `${stop.color} ${Math.round(startPos)}% ${Math.round(endPos)}%`;
				})
				.join(", ");

			return `linear-gradient(${Math.round(angle)}deg, ${softStops})`;
		}

		return "";
	};

	// Generate background pattern CSS
	const generateBackgroundPatternCSS = () => {
		const pattern = backgroundPatternPresets.find(
			(p) => p.id === gradient.backgroundPattern.id
		);
		if (!pattern || !pattern.css) return "";

		let css = `background-image: ${pattern.css};`;
		if (pattern.backgroundSize) {
			css += `\nbackground-size: ${pattern.backgroundSize};`;
		}
		if (pattern.backgroundPosition) {
			css += `\nbackground-position: ${pattern.backgroundPosition};`;
		}
		return css;
	};

	// Get background pattern style object for inline styles
	const getBackgroundPatternStyle = () => {
		const gradientCSS = generateGradientCSS();
		const pattern = backgroundPatternPresets.find(
			(p) => p.id === gradient.backgroundPattern.id
		);

		// If no pattern or pattern is "None", just return gradient
		if (!pattern || !pattern.css) {
			return {
				background: gradientCSS,
			};
		}

		// Combine gradient and pattern in background-image
		const combinedBackground = `${gradientCSS}, ${pattern.css}`;

		const style = {
			backgroundImage: combinedBackground,
		};
		if (pattern.backgroundSize) {
			// Combine background sizes: gradient uses auto, pattern uses its size
			style.backgroundSize = `auto, ${pattern.backgroundSize}`;
		}
		if (pattern.backgroundPosition) {
			// Combine background positions
			style.backgroundPosition = `0 0, ${pattern.backgroundPosition}`;
		}
		return style;
	};

	const generateAnimationCSS = () => {
		const {
			type: bgType,
			direction: bgDirection,
			speed,
			easing: bgEasing,
			repeat,
		} = gradient.backgroundAnimation;

		let backgroundAnimation = "";

		if (gradient.backgroundAnimation.enabled) {
			const infinite = repeat ? "infinite" : "1";
			switch (bgType) {
				case "slide":
					if (bgDirection === "right") {
						backgroundAnimation = `slideRight ${speed}s ${bgEasing} ${infinite}`;
					} else if (bgDirection === "left") {
						backgroundAnimation = `slideLeft ${speed}s ${bgEasing} ${infinite}`;
					} else if (bgDirection === "up") {
						backgroundAnimation = `slideUp ${speed}s ${bgEasing} ${infinite}`;
					} else {
						backgroundAnimation = `slideDown ${speed}s ${bgEasing} ${infinite}`;
					}
					break;
				case "wave":
					backgroundAnimation = `wave ${speed}s ${bgEasing} ${infinite}`;
					break;
			}
		}

		return backgroundAnimation;
	};

	const copyToClipboard = async (text, type) => {
		try {
			await navigator.clipboard.writeText(text);
			setCopied(type);
			setTimeout(() => setCopied(""), 2000);
		} catch (err) {
			console.error("Failed to copy: ", err);
		}
	};

	// Simple SVG download
	const downloadSVG = (dimensionType = downloadDimension) => {
		const dimensions =
			previewFramePresets[dimensionType] ||
			dimensionPresets[dimensionType] ||
			dimensionPresets.mobile;
		const width = dimensions.width;
		const height = dimensions.height;

		// Handle empty case - return white background SVG
		if (gradient.stops.length === 0) {
			const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
  <rect width="100%" height="100%" fill="#ffffff" />
</svg>`;
			const blob = new Blob([svg], { type: "image/svg+xml" });
			const url = URL.createObjectURL(blob);
			const link = document.createElement("a");
			link.href = url;
			link.download = `gradient-${dimensionType}-${width}x${height}-${Date.now()}.svg`;
			document.body.appendChild(link);
			link.click();
			document.body.removeChild(link);
			URL.revokeObjectURL(url);
			return;
		}

		const sortedStops = [...gradient.stops].sort((a, b) => {
			const distA = Math.sqrt(
				a.position.x * a.position.x + a.position.y * a.position.y
			);
			const distB = Math.sqrt(
				b.position.x * b.position.x + b.position.y * b.position.y
			);
			return distA - distB;
		});

		let gradientElement = "";
		if (gradient.type === "linear") {
			const firstStop = sortedStops[0];
			const lastStop = sortedStops[sortedStops.length - 1];
			const angle =
				Math.atan2(
					lastStop.position.y - firstStop.position.y,
					lastStop.position.x - firstStop.position.x
				) *
				(180 / Math.PI);

			const radians = (angle * Math.PI) / 180;
			const x1 = 50 - 50 * Math.cos(radians);
			const y1 = 50 - 50 * Math.sin(radians);
			const x2 = 50 + 50 * Math.cos(radians);
			const y2 = 50 + 50 * Math.sin(radians);

			gradientElement = `
      <linearGradient id="gradientFill" x1="${x1}%" y1="${y1}%" x2="${x2}%" y2="${y2}%">
        ${sortedStops
					.map(
						(stop) =>
							`<stop offset="${stop.position.x}%" stop-color="${stop.color}" />`
					)
					.join("\n        ")}
      </linearGradient>`;
		} else if (gradient.type === "radial") {
			gradientElement = `
      <radialGradient id="gradientFill" cx="50%" cy="50%" r="50%">
        ${sortedStops
					.map((stop) => {
						const distance = Math.sqrt(
							Math.pow(stop.position.x - 50, 2) +
								Math.pow(stop.position.y - 50, 2)
						);
						return `<stop offset="${distance}%" stop-color="${stop.color}" />`;
					})
					.join("\n        ")}
      </radialGradient>`;
		} else if (gradient.type === "conic") {
			gradientElement = `
      <linearGradient id="gradientFill" x1="50%" y1="0%" x2="50%" y2="100%">
        ${sortedStops
					.map(
						(stop) =>
							`<stop offset="${stop.position.x}%" stop-color="${stop.color}" />`
					)
					.join("\n        ")}
      </linearGradient>`;
		} else if (gradient.type === "mesh") {
			// For mesh, use multiple radial gradients
			const meshGradients = sortedStops
				.map(
					(stop, index) => `
      <radialGradient id="meshGradient${index}" cx="${stop.position.x}%" cy="${stop.position.y}%" r="50%">
        <stop offset="0%" stop-color="${stop.color}" stop-opacity="1" />
        <stop offset="50%" stop-color="${stop.color}" stop-opacity="0.5" />
        <stop offset="100%" stop-color="${stop.color}" stop-opacity="0" />
      </radialGradient>`
				)
				.join("");
			gradientElement = meshGradients;
		} else if (gradient.type === "grid") {
			const firstStop = sortedStops[0];
			const lastStop = sortedStops[sortedStops.length - 1];
			const angle =
				Math.atan2(
					lastStop.position.y - firstStop.position.y,
					lastStop.position.x - firstStop.position.x
				) *
				(180 / Math.PI);
			const radians = (angle * Math.PI) / 180;
			const x1 = 50 - 50 * Math.cos(radians);
			const y1 = 50 - 50 * Math.sin(radians);
			const x2 = 50 + 50 * Math.cos(radians);
			const y2 = 50 + 50 * Math.sin(radians);

			gradientElement = `
      <linearGradient id="gradientFill" x1="${x1}%" y1="${y1}%" x2="${x2}%" y2="${y2}%" gradientUnits="userSpaceOnUse">
        ${sortedStops
					.map(
						(stop) =>
							`<stop offset="${stop.position.x}%" stop-color="${stop.color}" />`
					)
					.join("\n        ")}
      </linearGradient>`;
		} else if (gradient.type === "sharp") {
			const firstStop = sortedStops[0];
			const lastStop = sortedStops[sortedStops.length - 1];
			const angle =
				Math.atan2(
					lastStop.position.y - firstStop.position.y,
					lastStop.position.x - firstStop.position.x
				) *
				(180 / Math.PI);
			const radians = (angle * Math.PI) / 180;
			const x1 = 50 - 50 * Math.cos(radians);
			const y1 = 50 - 50 * Math.sin(radians);
			const x2 = 50 + 50 * Math.cos(radians);
			const y2 = 50 + 50 * Math.sin(radians);

			const sharpStops = sortedStops.flatMap((stop, index) => {
				if (index === sortedStops.length - 1) {
					return [
						`<stop offset="${stop.position.x}%" stop-color="${stop.color}" />`,
					];
				}
				const nextStop = sortedStops[index + 1];
				const midPoint = (stop.position.x + nextStop.position.x) / 2;
				return [
					`<stop offset="${stop.position.x}%" stop-color="${stop.color}" />`,
					`<stop offset="${midPoint}%" stop-color="${stop.color}" />`,
					`<stop offset="${midPoint}%" stop-color="${nextStop.color}" />`,
				];
			});

			gradientElement = `
      <linearGradient id="gradientFill" x1="${x1}%" y1="${y1}%" x2="${x2}%" y2="${y2}%">
        ${sharpStops.join("\n        ")}
      </linearGradient>`;
		} else if (gradient.type === "soft") {
			const firstStop = sortedStops[0];
			const lastStop = sortedStops[sortedStops.length - 1];
			const angle =
				Math.atan2(
					lastStop.position.y - firstStop.position.y,
					lastStop.position.x - firstStop.position.x
				) *
				(180 / Math.PI);
			const radians = (angle * Math.PI) / 180;
			const x1 = 50 - 50 * Math.cos(radians);
			const y1 = 50 - 50 * Math.sin(radians);
			const x2 = 50 + 50 * Math.cos(radians);
			const y2 = 50 + 50 * Math.sin(radians);

			const softStops = sortedStops.map((stop, index) => {
				if (index === 0) {
					return `<stop offset="${Math.max(0, Math.round(stop.position.x) - 10)}%" stop-color="${stop.color}" />`;
				}
				if (index === sortedStops.length - 1) {
					return `<stop offset="${Math.min(100, Math.round(stop.position.x) + 10)}%" stop-color="${stop.color}" />`;
				}
				const prevStop = sortedStops[index - 1];
				const nextStop = sortedStops[index + 1];
				const startPos = Math.max(
					prevStop.position.x,
					Math.round(stop.position.x) - 15
				);
				const endPos = Math.min(
					nextStop.position.x,
					Math.round(stop.position.x) + 15
				);
				return `<stop offset="${Math.round(startPos)}%" stop-color="${stop.color}" />
        <stop offset="${Math.round(endPos)}%" stop-color="${stop.color}" />`;
			});

			gradientElement = `
      <linearGradient id="gradientFill" x1="${x1}%" y1="${y1}%" x2="${x2}%" y2="${y2}%">
        ${softStops.join("\n        ")}
      </linearGradient>`;
		} else {
			// Default to radial
			gradientElement = `
      <radialGradient id="gradientFill" cx="50%" cy="50%" r="50%">
        ${sortedStops
					.map((stop) => {
						const distance = Math.sqrt(
							Math.pow(stop.position.x - 50, 2) +
								Math.pow(stop.position.y - 50, 2)
						);
						return `<stop offset="${distance}%" stop-color="${stop.color}" />`;
					})
					.join("\n        ")}
      </radialGradient>`;
		}

		// For mesh gradients, we need multiple rects
		let svgContent = "";
		if (gradient.type === "mesh") {
			svgContent = sortedStops
				.map(
					(stop, index) => `
  <rect width="100%" height="100%" fill="url(#meshGradient${index})" />`
				)
				.join("");
		} else {
			svgContent = `<rect width="100%" height="100%" fill="url(#gradientFill)" />`;
		}

		const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
  <defs>${gradientElement}
  </defs>${svgContent}
</svg>`;

		const blob = new Blob([svg], { type: "image/svg+xml" });
		const url = URL.createObjectURL(blob);
		const link = document.createElement("a");
		link.href = url;
		link.download = `gradient-${dimensionType}-${width}x${height}-${Date.now()}.svg`;
		document.body.appendChild(link);
		link.click();
		document.body.removeChild(link);
		URL.revokeObjectURL(url);
	};

	// Canvas-based download for PNG/JPEG
	const downloadRaster = (
		format = "png",
		dimensionType = downloadDimension
	) => {
		const dimensions =
			previewFramePresets[dimensionType] ||
			dimensionPresets[dimensionType] ||
			dimensionPresets.mobile;
		const canvas = document.createElement("canvas");
		const width = dimensions.width;
		const height = dimensions.height;
		canvas.width = width;
		canvas.height = height;
		const ctx = canvas.getContext("2d");

		// Handle empty case - fill with white background
		if (gradient.stops.length === 0) {
			ctx.fillStyle = "#ffffff";
			ctx.fillRect(0, 0, width, height);
			canvas.toBlob(
				(blob) => {
					if (!blob) return;
					const url = URL.createObjectURL(blob);
					const link = document.createElement("a");
					link.href = url;
					link.download = `gradient-${dimensionType}-${width}x${height}-${Date.now()}.${format}`;
					document.body.appendChild(link);
					link.click();
					document.body.removeChild(link);
					URL.revokeObjectURL(url);
				},
				format === "jpeg" ? "image/jpeg" : "image/png",
				format === "jpeg" ? 0.92 : undefined
			);
			return;
		}

		let gradientObj;
		const sortedStops = [...gradient.stops].sort((a, b) => {
			const distA = Math.sqrt(
				a.position.x * a.position.x + a.position.y * a.position.y
			);
			const distB = Math.sqrt(
				b.position.x * b.position.x + b.position.y * b.position.y
			);
			return distA - distB;
		});

		if (gradient.type === "linear") {
			const firstStop = sortedStops[0];
			const lastStop = sortedStops[sortedStops.length - 1];
			const angle =
				Math.atan2(
					lastStop.position.y - firstStop.position.y,
					lastStop.position.x - firstStop.position.x
				) *
				(180 / Math.PI);

			const radians = (angle * Math.PI) / 180;
			const x1 = width / 2 - (width / 2) * Math.cos(radians);
			const y1 = height / 2 - (width / 2) * Math.sin(radians);
			const x2 = width / 2 + (width / 2) * Math.cos(radians);
			const y2 = height / 2 + (width / 2) * Math.sin(radians);

			gradientObj = ctx.createLinearGradient(x1, y1, x2, y2);
			sortedStops.forEach((stop) => {
				const position = Math.round(stop.position.x) / 100;
				gradientObj.addColorStop(position, stop.color);
			});
		} else if (gradient.type === "radial") {
			gradientObj = ctx.createRadialGradient(
				width / 2,
				height / 2,
				0,
				width / 2,
				height / 2,
				Math.max(width, height) / 2
			);
			sortedStops.forEach((stop) => {
				const distance = Math.sqrt(
					Math.pow(stop.position.x - 50, 2) + Math.pow(stop.position.y - 50, 2)
				);
				const position = Math.round(distance) / 100;
				gradientObj.addColorStop(position, stop.color);
			});
		} else if (gradient.type === "conic") {
			// Conic gradients are approximated with linear gradient
			gradientObj = ctx.createLinearGradient(width / 2, 0, width / 2, height);
			sortedStops.forEach((stop) => {
				const position = Math.round(stop.position.x) / 100;
				gradientObj.addColorStop(position, stop.color);
			});
		} else if (gradient.type === "mesh") {
			// For mesh, draw multiple radial gradients
			ctx.globalCompositeOperation = "multiply";
			sortedStops.forEach((stop) => {
				const meshGradient = ctx.createRadialGradient(
					(stop.position.x / 100) * width,
					(stop.position.y / 100) * height,
					0,
					(stop.position.x / 100) * width,
					(stop.position.y / 100) * height,
					Math.max(width, height) / 2
				);
				meshGradient.addColorStop(0, stop.color);
				meshGradient.addColorStop(0.5, stop.color);
				meshGradient.addColorStop(1, "transparent");
				ctx.fillStyle = meshGradient;
				ctx.fillRect(0, 0, width, height);
			});
			ctx.globalCompositeOperation = "source-over";
			return; // Early return for mesh
		} else if (gradient.type === "grid") {
			const firstStop = sortedStops[0];
			const lastStop = sortedStops[sortedStops.length - 1];
			const angle =
				Math.atan2(
					lastStop.position.y - firstStop.position.y,
					lastStop.position.x - firstStop.position.x
				) *
				(180 / Math.PI);

			const radians = (angle * Math.PI) / 180;
			const x1 = width / 2 - (width / 2) * Math.cos(radians);
			const y1 = height / 2 - (width / 2) * Math.sin(radians);
			const x2 = width / 2 + (width / 2) * Math.cos(radians);
			const y2 = height / 2 + (width / 2) * Math.sin(radians);

			// Create pattern for repeating gradient
			const patternCanvas = document.createElement("canvas");
			patternCanvas.width = width;
			patternCanvas.height = height;
			const patternCtx = patternCanvas.getContext("2d");
			const patternGradient = patternCtx.createLinearGradient(x1, y1, x2, y2);
			sortedStops.forEach((stop) => {
				const position = Math.round(stop.position.x) / 100;
				patternGradient.addColorStop(position, stop.color);
			});
			patternCtx.fillStyle = patternGradient;
			patternCtx.fillRect(0, 0, width, height);
			const pattern = ctx.createPattern(patternCanvas, "repeat");
			ctx.fillStyle = pattern;
		} else if (gradient.type === "sharp") {
			const firstStop = sortedStops[0];
			const lastStop = sortedStops[sortedStops.length - 1];
			const angle =
				Math.atan2(
					lastStop.position.y - firstStop.position.y,
					lastStop.position.x - firstStop.position.x
				) *
				(180 / Math.PI);

			const radians = (angle * Math.PI) / 180;
			const x1 = width / 2 - (width / 2) * Math.cos(radians);
			const y1 = height / 2 - (width / 2) * Math.sin(radians);
			const x2 = width / 2 + (width / 2) * Math.cos(radians);
			const y2 = height / 2 + (width / 2) * Math.sin(radians);

			gradientObj = ctx.createLinearGradient(x1, y1, x2, y2);
			sortedStops.forEach((stop, index) => {
				const position = Math.round(stop.position.x) / 100;
				gradientObj.addColorStop(position, stop.color);
				if (index < sortedStops.length - 1) {
					const nextStop = sortedStops[index + 1];
					const midPoint = (stop.position.x + nextStop.position.x) / 200;
					gradientObj.addColorStop(midPoint, stop.color);
					gradientObj.addColorStop(midPoint, nextStop.color);
				}
			});
		} else if (gradient.type === "soft") {
			const firstStop = sortedStops[0];
			const lastStop = sortedStops[sortedStops.length - 1];
			const angle =
				Math.atan2(
					lastStop.position.y - firstStop.position.y,
					lastStop.position.x - firstStop.position.x
				) *
				(180 / Math.PI);

			const radians = (angle * Math.PI) / 180;
			const x1 = width / 2 - (width / 2) * Math.cos(radians);
			const y1 = height / 2 - (width / 2) * Math.sin(radians);
			const x2 = width / 2 + (width / 2) * Math.cos(radians);
			const y2 = height / 2 + (width / 2) * Math.sin(radians);

			gradientObj = ctx.createLinearGradient(x1, y1, x2, y2);
			sortedStops.forEach((stop, index) => {
				if (index === 0) {
					gradientObj.addColorStop(
						Math.max(0, (Math.round(stop.position.x) - 10) / 100),
						stop.color
					);
				} else if (index === sortedStops.length - 1) {
					gradientObj.addColorStop(
						Math.min(1, (Math.round(stop.position.x) + 10) / 100),
						stop.color
					);
				} else {
					const prevStop = sortedStops[index - 1];
					const nextStop = sortedStops[index + 1];
					const startPos = Math.max(
						prevStop.position.x / 100,
						(Math.round(stop.position.x) - 15) / 100
					);
					const endPos = Math.min(
						nextStop.position.x / 100,
						(Math.round(stop.position.x) + 15) / 100
					);
					gradientObj.addColorStop(startPos, stop.color);
					gradientObj.addColorStop(endPos, stop.color);
				}
			});
		} else {
			// Default to radial
			gradientObj = ctx.createRadialGradient(
				width / 2,
				height / 2,
				0,
				width / 2,
				height / 2,
				Math.max(width, height) / 2
			);
			sortedStops.forEach((stop) => {
				const distance = Math.sqrt(
					Math.pow(stop.position.x - 50, 2) + Math.pow(stop.position.y - 50, 2)
				);
				const position = Math.round(distance) / 100;
				gradientObj.addColorStop(position, stop.color);
			});
		}

		if (gradientObj) {
			ctx.fillStyle = gradientObj;
			ctx.fillRect(0, 0, width, height);
		}

		const mimeType = format === "jpeg" ? "image/jpeg" : "image/png";
		const quality = format === "jpeg" ? 0.92 : undefined;

		canvas.toBlob(
			(blob) => {
				if (!blob) return;
				const url = URL.createObjectURL(blob);
				const link = document.createElement("a");
				link.href = url;
				link.download = `gradient-${dimensionType}-${width}x${height}-${Date.now()}.${format}`;
				document.body.appendChild(link);
				link.click();
				document.body.removeChild(link);
				URL.revokeObjectURL(url);
			},
			mimeType,
			quality
		);
	};

	// GIF download function
	const downloadGIF = async (dimensionType = downloadDimension) => {
		alert(
			"GIF export is being processed. This feature will capture animated frames of your gradient."
		);
	};

	// MP4 download function
	const downloadMP4 = async (dimensionType = downloadDimension) => {
		if (typeof window === "undefined") return;

		// Check if animation is enabled - if not, download as PNG instead
		if (!gradient.backgroundAnimation.enabled) {
			// Download as PNG since there's no animation
			downloadRaster("png", dimensionType);
			return;
		}

		setIsGeneratingMP4(true);
		setMp4Progress(0);

		try {
			const dimensions =
				previewFramePresets[dimensionType] ||
				dimensionPresets[dimensionType] ||
				dimensionPresets.mobile;
			const width = dimensions.width;
			const height = dimensions.height;

			setMp4Progress(10);

			// Create a canvas for recording
			const canvas = document.createElement("canvas");
			canvas.width = width;
			canvas.height = height;
			canvas.style.position = "fixed";
			canvas.style.top = "-9999px";
			canvas.style.left = "-9999px";
			document.body.appendChild(canvas);
			const ctx = canvas.getContext("2d");

			// Get canvas stream for MediaRecorder
			const stream = canvas.captureStream(30); // 30 FPS
			const mediaRecorder = new MediaRecorder(stream, {
				mimeType: "video/webm;codecs=vp9",
			});

			const chunks = [];

			mediaRecorder.ondataavailable = (event) => {
				if (event.data.size > 0) {
					chunks.push(event.data);
				}
			};

			mediaRecorder.onstop = () => {
				const blob = new Blob(chunks, { type: "video/webm" });
				const url = URL.createObjectURL(blob);
				const link = document.createElement("a");
				link.href = url;
				link.download = `gradient-${dimensionType}-${width}x${height}-${Date.now()}.webm`;
				document.body.appendChild(link);
				link.click();
				document.body.removeChild(link);
				URL.revokeObjectURL(url);
				document.body.removeChild(canvas);
				setIsGeneratingMP4(false);
				setMp4Progress(0);
			};

			// Calculate animation parameters
			const animationSpeed = gradient.backgroundAnimation.speed;
			const animationType = gradient.backgroundAnimation.type;
			const animationDirection = gradient.backgroundAnimation.direction;
			const duration = 5; // 5 seconds of video
			const fps = 30;
			const totalFrames = duration * fps;

			const drawGradient = (progress = 0) => {
				// Clear canvas
				ctx.clearRect(0, 0, width, height);

				// Handle empty case - fill with white background
				if (gradient.stops.length === 0) {
					ctx.fillStyle = "#ffffff";
					ctx.fillRect(0, 0, width, height);
					return;
				}

				// Calculate background position based on animation
				let bgX = 0;
				let bgY = 0;
				const bgSize = 200; // 200% for animation

				if (animationType === "slide") {
					if (animationDirection === "right") {
						bgX = progress * bgSize;
					} else if (animationDirection === "left") {
						bgX = (1 - progress) * bgSize;
					} else if (animationDirection === "up") {
						bgY = (1 - progress) * bgSize;
					} else if (animationDirection === "down") {
						bgY = progress * bgSize;
					}
				} else if (animationType === "wave") {
					bgX = Math.sin(progress * Math.PI * 2) * bgSize;
					bgY = Math.cos(progress * Math.PI * 2) * bgSize;
				}

				// Save context for background pattern
				ctx.save();

				// Draw gradient
				let gradientObj;
				const sortedStops = [...gradient.stops].sort((a, b) => {
					const distA = Math.sqrt(
						a.position.x * a.position.x + a.position.y * a.position.y
					);
					const distB = Math.sqrt(
						b.position.x * b.position.x + b.position.y * b.position.y
					);
					return distA - distB;
				});

				if (gradient.type === "linear") {
					const firstStop = sortedStops[0];
					const lastStop = sortedStops[sortedStops.length - 1];
					const angle =
						Math.atan2(
							lastStop.position.y - firstStop.position.y,
							lastStop.position.x - firstStop.position.x
						) *
						(180 / Math.PI);

					const radians = (angle * Math.PI) / 180;
					const x1 = width / 2 - (width / 2) * Math.cos(radians);
					const y1 = height / 2 - (width / 2) * Math.sin(radians);
					const x2 = width / 2 + (width / 2) * Math.cos(radians);
					const y2 = height / 2 + (width / 2) * Math.sin(radians);

					gradientObj = ctx.createLinearGradient(x1, y1, x2, y2);
					sortedStops.forEach((stop) => {
						const position = Math.round(stop.position.x) / 100;
						gradientObj.addColorStop(position, stop.color);
					});
				} else if (gradient.type === "radial") {
					gradientObj = ctx.createRadialGradient(
						width / 2,
						height / 2,
						0,
						width / 2,
						height / 2,
						Math.max(width, height) / 2
					);
					sortedStops.forEach((stop) => {
						const distance = Math.sqrt(
							Math.pow(stop.position.x - 50, 2) +
								Math.pow(stop.position.y - 50, 2)
						);
						const position = Math.round(distance) / 100;
						gradientObj.addColorStop(position, stop.color);
					});
				} else if (gradient.type === "conic") {
					gradientObj = ctx.createLinearGradient(
						width / 2,
						0,
						width / 2,
						height
					);
					sortedStops.forEach((stop) => {
						const position = Math.round(stop.position.x) / 100;
						gradientObj.addColorStop(position, stop.color);
					});
				} else if (gradient.type === "mesh") {
					ctx.globalCompositeOperation = "multiply";
					sortedStops.forEach((stop) => {
						const meshGradient = ctx.createRadialGradient(
							(stop.position.x / 100) * width,
							(stop.position.y / 100) * height,
							0,
							(stop.position.x / 100) * width,
							(stop.position.y / 100) * height,
							Math.max(width, height) / 2
						);
						meshGradient.addColorStop(0, stop.color);
						meshGradient.addColorStop(0.5, stop.color);
						meshGradient.addColorStop(1, "transparent");
						ctx.fillStyle = meshGradient;
						ctx.fillRect(0, 0, width, height);
					});
					ctx.globalCompositeOperation = "source-over";
					ctx.restore();
					return;
				} else if (gradient.type === "grid") {
					const firstStop = sortedStops[0];
					const lastStop = sortedStops[sortedStops.length - 1];
					const angle =
						Math.atan2(
							lastStop.position.y - firstStop.position.y,
							lastStop.position.x - firstStop.position.x
						) *
						(180 / Math.PI);

					const radians = (angle * Math.PI) / 180;
					const x1 = width / 2 - (width / 2) * Math.cos(radians);
					const y1 = height / 2 - (width / 2) * Math.sin(radians);
					const x2 = width / 2 + (width / 2) * Math.cos(radians);
					const y2 = height / 2 + (width / 2) * Math.sin(radians);

					const patternCanvas = document.createElement("canvas");
					patternCanvas.width = width;
					patternCanvas.height = height;
					const patternCtx = patternCanvas.getContext("2d");
					const patternGradient = patternCtx.createLinearGradient(
						x1,
						y1,
						x2,
						y2
					);
					sortedStops.forEach((stop) => {
						const position = Math.round(stop.position.x) / 100;
						patternGradient.addColorStop(position, stop.color);
					});
					patternCtx.fillStyle = patternGradient;
					patternCtx.fillRect(0, 0, width, height);
					const pattern = ctx.createPattern(patternCanvas, "repeat");
					ctx.fillStyle = pattern;
				} else if (gradient.type === "sharp") {
					const firstStop = sortedStops[0];
					const lastStop = sortedStops[sortedStops.length - 1];
					const angle =
						Math.atan2(
							lastStop.position.y - firstStop.position.y,
							lastStop.position.x - firstStop.position.x
						) *
						(180 / Math.PI);

					const radians = (angle * Math.PI) / 180;
					const x1 = width / 2 - (width / 2) * Math.cos(radians);
					const y1 = height / 2 - (width / 2) * Math.sin(radians);
					const x2 = width / 2 + (width / 2) * Math.cos(radians);
					const y2 = height / 2 + (width / 2) * Math.sin(radians);

					gradientObj = ctx.createLinearGradient(x1, y1, x2, y2);
					sortedStops.forEach((stop, index) => {
						const position = Math.round(stop.position.x) / 100;
						gradientObj.addColorStop(position, stop.color);
						if (index < sortedStops.length - 1) {
							const nextStop = sortedStops[index + 1];
							const midPoint = (stop.position.x + nextStop.position.x) / 200;
							gradientObj.addColorStop(midPoint, stop.color);
							gradientObj.addColorStop(midPoint, nextStop.color);
						}
					});
				} else if (gradient.type === "soft") {
					const firstStop = sortedStops[0];
					const lastStop = sortedStops[sortedStops.length - 1];
					const angle =
						Math.atan2(
							lastStop.position.y - firstStop.position.y,
							lastStop.position.x - firstStop.position.x
						) *
						(180 / Math.PI);

					const radians = (angle * Math.PI) / 180;
					const x1 = width / 2 - (width / 2) * Math.cos(radians);
					const y1 = height / 2 - (width / 2) * Math.sin(radians);
					const x2 = width / 2 + (width / 2) * Math.cos(radians);
					const y2 = height / 2 + (width / 2) * Math.sin(radians);

					gradientObj = ctx.createLinearGradient(x1, y1, x2, y2);
					sortedStops.forEach((stop, index) => {
						if (index === 0) {
							gradientObj.addColorStop(
								Math.max(0, (Math.round(stop.position.x) - 10) / 100),
								stop.color
							);
						} else if (index === sortedStops.length - 1) {
							gradientObj.addColorStop(
								Math.min(1, (Math.round(stop.position.x) + 10) / 100),
								stop.color
							);
						} else {
							const prevStop = sortedStops[index - 1];
							const nextStop = sortedStops[index + 1];
							const startPos = Math.max(
								prevStop.position.x / 100,
								(Math.round(stop.position.x) - 15) / 100
							);
							const endPos = Math.min(
								nextStop.position.x / 100,
								(Math.round(stop.position.x) + 15) / 100
							);
							gradientObj.addColorStop(startPos, stop.color);
							gradientObj.addColorStop(endPos, stop.color);
						}
					});
				} else {
					gradientObj = ctx.createRadialGradient(
						width / 2,
						height / 2,
						0,
						width / 2,
						height / 2,
						Math.max(width, height) / 2
					);
					sortedStops.forEach((stop) => {
						const distance = Math.sqrt(
							Math.pow(stop.position.x - 50, 2) +
								Math.pow(stop.position.y - 50, 2)
						);
						const position = Math.round(distance) / 100;
						gradientObj.addColorStop(position, stop.color);
					});
				}

				if (gradientObj) {
					// Apply background size for animation
					ctx.save();
					ctx.translate(-bgX, -bgY);
					ctx.scale(2, 2); // 200% size
					ctx.fillStyle = gradientObj;
					ctx.fillRect(bgX / 2, bgY / 2, width, height);
					ctx.restore();
				}

				// Draw background pattern if enabled
				const pattern = backgroundPatternPresets.find(
					(p) => p.id === gradient.backgroundPattern.id
				);
				if (pattern && pattern.css) {
					// For patterns, we'll draw them on top
					// This is a simplified version - full pattern rendering would be more complex
					ctx.restore();
				} else {
					ctx.restore();
				}
			};

			setMp4Progress(20);

			// Start recording
			mediaRecorder.start();

			// Record frames
			for (let frame = 0; frame < totalFrames; frame++) {
				const progress = (frame / totalFrames) * (duration / animationSpeed);
				const normalizedProgress = progress % 1; // Loop animation
				drawGradient(normalizedProgress);

				// Update progress
				const frameProgress = 20 + (frame / totalFrames) * 70;
				setMp4Progress(Math.min(95, frameProgress));

				// Wait for next frame
				await new Promise((resolve) => setTimeout(resolve, 1000 / fps));
			}

			setMp4Progress(95);

			// Stop recording
			mediaRecorder.stop();
		} catch (error) {
			console.error("Error generating MP4:", error);
			setIsGeneratingMP4(false);
			setMp4Progress(0);
			alert(
				"Error generating video. Your browser may not support video recording. Please try downloading as PNG instead."
			);
		}
	};

	// Handle click outside gradient presets dropdown
	const handleGradientPresetsClickOutside = useCallback((event) => {
		if (
			gradientPresetsRef.current &&
			!gradientPresetsRef.current.contains(event.target)
		) {
			setIsGradientPresetsOpen(false);
		}
	}, []);

	useEffect(() => {
		if (isGradientPresetsOpen) {
			document.addEventListener("mousedown", handleGradientPresetsClickOutside);
			return () =>
				document.removeEventListener(
					"mousedown",
					handleGradientPresetsClickOutside
				);
		}
	}, [isGradientPresetsOpen, handleGradientPresetsClickOutside]);

	// Handle click outside background patterns dropdown
	const handleBackgroundPatternsClickOutside = useCallback((event) => {
		if (
			backgroundPatternsRef.current &&
			!backgroundPatternsRef.current.contains(event.target)
		) {
			setIsBackgroundPatternsOpen(false);
		}
	}, []);

	useEffect(() => {
		if (isBackgroundPatternsOpen) {
			document.addEventListener(
				"mousedown",
				handleBackgroundPatternsClickOutside
			);
			return () =>
				document.removeEventListener(
					"mousedown",
					handleBackgroundPatternsClickOutside
				);
		}
	}, [isBackgroundPatternsOpen, handleBackgroundPatternsClickOutside]);

	// Handle click outside download dropdown
	const handleDownloadDropdownClickOutside = useCallback((event) => {
		if (
			downloadDropdownRef.current &&
			!downloadDropdownRef.current.contains(event.target)
		) {
			setIsDownloadDropdownOpen(false);
		}
	}, []);

	useEffect(() => {
		if (isDownloadDropdownOpen) {
			document.addEventListener(
				"mousedown",
				handleDownloadDropdownClickOutside
			);
			return () =>
				document.removeEventListener(
					"mousedown",
					handleDownloadDropdownClickOutside
				);
		}
	}, [isDownloadDropdownOpen, handleDownloadDropdownClickOutside]);

	// Handle click outside panel download dropdown
	const handlePanelDownloadDropdownClickOutside = useCallback((event) => {
		if (
			panelDownloadDropdownRef.current &&
			!panelDownloadDropdownRef.current.contains(event.target)
		) {
			setIsPanelDownloadDropdownOpen(false);
		}
	}, []);

	useEffect(() => {
		if (isPanelDownloadDropdownOpen) {
			document.addEventListener(
				"mousedown",
				handlePanelDownloadDropdownClickOutside
			);
			return () =>
				document.removeEventListener(
					"mousedown",
					handlePanelDownloadDropdownClickOutside
				);
		}
	}, [isPanelDownloadDropdownOpen, handlePanelDownloadDropdownClickOutside]);

	const backgroundAnimation = useMemo(
		() => generateAnimationCSS(),
		[gradient.backgroundAnimation]
	);

	// Calculate modal preview dimensions
	const modalDimensions = useMemo(() => {
		const { width: frameWidth, height: frameHeight } = gradient.dimensions;
		const aspectRatio = frameWidth / frameHeight;

		if (aspectRatio >= 1) {
			return {
				width: "90vw",
				maxWidth: "90vw",
				maxHeight: "90vh",
			};
		} else {
			return {
				height: "90vh",
				maxWidth: "90vw",
				maxHeight: "90vh",
			};
		}
	}, [gradient.dimensions, previewFrameSize]);

	// Find active gradient preset
	const activePreset = useMemo(() => {
		return gradientPresets.find((preset) => {
			// Check if type and angle match
			if (preset.type !== gradient.type) return false;
			if (preset.type === "linear" && preset.angle !== gradient.angle) {
				return false;
			}

			// Check if stops match (compare colors and positions)
			if (preset.stops.length !== gradient.stops.length) return false;

			// Sort stops by position for comparison
			const sortedPresetStops = [...preset.stops].sort((a, b) => {
				const distA = Math.sqrt(
					a.position.x * a.position.x + a.position.y * a.position.y
				);
				const distB = Math.sqrt(
					b.position.x * b.position.x + b.position.y * b.position.y
				);
				return distA - distB;
			});

			const sortedGradientStops = [...gradient.stops].sort((a, b) => {
				const distA = Math.sqrt(
					a.position.x * a.position.x + a.position.y * a.position.y
				);
				const distB = Math.sqrt(
					b.position.x * b.position.x + b.position.y * b.position.y
				);
				return distA - distB;
			});

			// Compare each stop's color (allow small position differences)
			return sortedPresetStops.every((presetStop, index) => {
				const gradientStop = sortedGradientStops[index];
				return (
					presetStop.color.toLowerCase() === gradientStop.color.toLowerCase()
				);
			});
		});
	}, [gradient.type, gradient.angle, gradient.stops]);

	// Get current preset index
	const currentPresetIndex = useMemo(() => {
		if (!activePreset) return -1;
		return gradientPresets.findIndex((p) => p.id === activePreset.id);
	}, [activePreset]);

	// Navigate to next preset
	const navigateToNextPreset = useCallback(() => {
		const currentIndex = currentPresetIndex >= 0 ? currentPresetIndex : 0;
		const nextIndex = (currentIndex + 1) % gradientPresets.length;
		const nextPreset = gradientPresets[nextIndex];

		setGradient((prev) => ({
			...prev,
			type: nextPreset.type,
			angle: nextPreset.angle,
			stops: nextPreset.stops.map((stop) => ({
				...stop,
			})),
		}));
	}, [currentPresetIndex]);

	// Navigate to previous preset
	const navigateToPreviousPreset = useCallback(() => {
		const currentIndex = currentPresetIndex >= 0 ? currentPresetIndex : 0;
		const prevIndex =
			currentIndex === 0 ? gradientPresets.length - 1 : currentIndex - 1;
		const prevPreset = gradientPresets[prevIndex];

		setGradient((prev) => ({
			...prev,
			type: prevPreset.type,
			angle: prevPreset.angle,
			stops: prevPreset.stops.map((stop) => ({
				...stop,
			})),
		}));
	}, [currentPresetIndex]);

	// Keyboard navigation for presets
	useEffect(() => {
		const handleKeyPress = (e) => {
			// Only trigger if not typing in an input/textarea
			if (
				e.target.tagName === "INPUT" ||
				e.target.tagName === "TEXTAREA" ||
				e.target.isContentEditable
			) {
				return;
			}

			// Arrow Right, Period (.), or Spacebar for next preset
			if (e.key === "ArrowRight" || e.key === "." || e.key === " ") {
				e.preventDefault();
				navigateToNextPreset();
			}
			// Arrow Left or Comma (,) for previous preset
			else if (e.key === "ArrowLeft" || e.key === ",") {
				e.preventDefault();
				navigateToPreviousPreset();
			}
		};

		window.addEventListener("keydown", handleKeyPress);
		return () => window.removeEventListener("keydown", handleKeyPress);
	}, [navigateToNextPreset, navigateToPreviousPreset]);

	return (
		<div className="min-h-screen bg-stone-100 p-6 canvas-dots-bg">
			<style jsx>{`
				.slider::-webkit-slider-thumb {
					appearance: none;
					height: 16px;
					width: 16px;
					border-radius: 50%;
					background: #71717a;
					cursor: pointer;
					border: 2px solid #ffffff;
					box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
				}
				.slider::-moz-range-thumb {
					height: 16px;
					width: 16px;
					border-radius: 50%;
					background: #71717a;
					cursor: pointer;
					border: 2px solid #ffffff;
					box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
				}

				@keyframes slideRight {
					0% {
						background-position: 0% 0%;
					}
					100% {
						background-position: 100% 0%;
					}
				}
				@keyframes slideLeft {
					0% {
						background-position: 100% 0%;
					}
					100% {
						background-position: 0% 0%;
					}
				}
				@keyframes slideUp {
					0% {
						background-position: 0% 100%;
					}
					100% {
						background-position: 0% 0%;
					}
				}
				@keyframes slideDown {
					0% {
						background-position: 0% 0%;
					}
					100% {
						background-position: 0% 100%;
					}
				}
				@keyframes wave {
					0% {
						background-position: 0% 0%;
					}
					25% {
						background-position: 100% 0%;
					}
					50% {
						background-position: 100% 100%;
					}
					75% {
						background-position: 0% 100%;
					}
					100% {
						background-position: 0% 0%;
					}
				}
				@keyframes meshGradient {
					0% {
						background-position: 0% 0%;
					}
					50% {
						background-position: 100% 100%;
					}
					100% {
						background-position: 0% 0%;
					}
				}

				.canvas-dots-bg {
					background-image: radial-gradient(
						circle,
						rgba(0, 0, 0, 0.15) 1px,
						transparent 1px
					);
					background-size: 20px 20px;
					background-position: 0 0;
				}
				.noise-overlay {
					position: absolute;
					top: 0;
					left: 0;
					width: 100%;
					height: 100%;
					pointer-events: none;
					opacity: var(--noise-opacity);
					background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
					background-size: 200px 200px;
					mix-blend-mode: overlay;
				}
			`}</style>
			<div className="max-w-7xl mx-auto">
				{/* Preview Header */}
				<div className="flex items-center justify-end gap-2 mb-4 flex-wrap">
					{/* Frame Size Selector */}
					<div className="flex ">
						<Dropdown
							value={previewFrameSize}
							onChange={(value) => {
								setPreviewFrameSize(value);
								const frame = previewFramePresets[value];
								if (frame) {
									setGradient((prev) => ({
										...prev,
										dimensions: {
											width: frame.width,
											height: frame.height,
										},
									}));
								}
							}}
							options={Object.entries(previewFramePresets).map(
								([key, preset]) => ({
									value: key,
									label: `${preset.icon} ${preset.label} (${preset.width}×${preset.height})`,
								})
							)}
							placeholder="Select frame size"
							className="min-w-[180px]"
						/>
					</div>
					{/* <button
						onClick={() => setIsModalOpen(true)}
						className="flex items-center gap-2 px-3 py-1.5 text-sm bg-zinc-100 hover:bg-zinc-200 rounded-xl transition-colors h-8"
					>
						<Maximize2 className="w-4 h-4" />
						Preview
					</button> */}
				</div>
				<div className="grid grid-cols-1 lg:grid-cols-3 w-full">
					{/* Preview Section */}
					<div className="space-y-6 col-span-2">
						<div className="bg-white rounded-xl shadow-lg">
							{/* Interactive Gradient Preview */}
							<div
								className="flex justify-center items-center w-full"
								style={{
									minHeight:
										gradient.dimensions.height > gradient.dimensions.width
											? "600px"
											: "400px",
								}}
							>
								<div
									ref={previewRef}
									className="relative rounded-xl overflow-hidden cursor-crosshair "
									style={{
										aspectRatio: `${gradient.dimensions.width} / ${gradient.dimensions.height}`,
										width: "100%",
										maxWidth:
											gradient.dimensions.width > gradient.dimensions.height
												? `${Math.min(gradient.dimensions.width * 0.6, 1200)}px`
												: "100%",
										maxHeight:
											gradient.dimensions.height > gradient.dimensions.width
												? "90vh"
												: `${Math.min(gradient.dimensions.height * 0.6, 600)}px`,
										...getBackgroundPatternStyle(),
										...(isPlaying &&
											gradient.backgroundAnimation.enabled && {
												backgroundSize: (() => {
													const patternStyle = getBackgroundPatternStyle();
													if (patternStyle.backgroundSize) {
														return patternStyle.backgroundSize.replace(
															"auto",
															"200% 200%"
														);
													}
													return "200% 200%";
												})(),
											}),
										...(isPlaying &&
											backgroundAnimation && {
												animation: backgroundAnimation,
											}),
									}}
								>
									{/* Noise Overlay */}
									{gradient.noise.enabled && (
										<div
											className="noise-overlay"
											style={{
												"--noise-opacity": gradient.noise.intensity,
											}}
										/>
									)}
									{/* Color Stop Handles */}
									{gradient.stops.map((stop) => (
										<div
											key={stop.id}
											className={`absolute w-6 h-6 flex items-center justify-center cursor-pointer group transition-transform ${
												selectedStop === stop.id
													? "scale-125"
													: "hover:scale-110"
											}`}
											style={{
												left: `calc(${stop.position.x}% - 12px)`,
												top: `calc(${stop.position.y}% - 12px)`,
											}}
											onMouseDown={(e) => handleMouseDown(e, stop.id)}
											onKeyDown={(e) => handleKeyDown(e, stop.id)}
											tabIndex={0}
											role="button"
											aria-label={`Color stop at ${Math.round(
												stop.position.x
											)}%, ${Math.round(stop.position.y)}%`}
										>
											<div
												className="w-6 h-6 rounded-full border-2 border-white shadow-lg hover:shadow-xl transition-shadow"
												style={{ backgroundColor: stop.color }}
											/>
											<div className="absolute -bottom-8 left-1/2 transform -translate-x-1/2 text-xs text-white bg-black bg-opacity-75 px-2 py-1 rounded whitespace-nowrap">
												{Math.round(stop.position.x)}%,{" "}
												{Math.round(stop.position.y)}%
											</div>
										</div>
									))}
								</div>
							</div>
						</div>
					</div>

					{/* Control Panel */}
					<div className="space-y-6 bg-white cols-span-1 ml-auto max-h-[80vh] max-w-xs overflow-y-auto hidescrollbar shadow-xl border border-zinc-100 rounded-xl">
						{/* Color Stops */}
						{/* Gradient Type & Controls */}
						<div className="bg-white rounded-xl shadow-lg p-4">
							<div className="flex items-center justify-between">
								<h3 className="text-sm">Colors</h3>
								<button
									onClick={addColorStop}
									className="flex items-center px-2 py-1 rounded-xl hover:bg-zinc-100 transition-colors text-xs h-8"
								>
									<Plus className="w-3 h-3" />
								</button>
							</div>
							<div className="mb-4">
								{gradient.stops.map((stop) => (
									<div
										key={stop.id}
										className="flex items-center gap-2 p-1 hover:bg-gray-50 rounded-xl"
									>
										<input
											type="color"
											value={stop.color}
											onChange={(e) =>
												updateColorStop(stop.id, "color", e.target.value)
											}
											className="w-6 h-6 rounded border border-gray-300 cursor-pointer"
											aria-label={`Color for stop at ${Math.round(
												stop.position.x
											)}%, ${Math.round(stop.position.y)}%`}
										/>
										<div className="flex-1 flex justify-between">
											<div className="text-xs text-gray-600 mb-0.5">
												Position: {Math.round(stop.position.x)}%,{" "}
												{Math.round(stop.position.y)}%
											</div>
											<div className="text-[10px] text-gray-500 font-mono">
												{stop.color}
											</div>
										</div>
										<button
											onClick={() => removeColorStop(stop.id)}
											className="p-1.5 text-red-500 hover:bg-red-50 rounded"
											aria-label={`Remove color stop`}
										>
											<Trash2 className="w-3 h-3" />
										</button>
									</div>
								))}
							</div>
							<div className="space-y-3">
								<div>
									<label className="flex justify-between items-center text-xs font-medium text-gray-700 mb-1.5">
										Gradient Templates
										<div className="relative group flex items-center gap-1">
											<span className="w-fit border border-gray-200 group-hover:opacity-100 opacity-0 transition-all duration-75 ease-in invisible group-hover:visible bg-white z-50 p-1 rounded-xl text-[10px] text-gray-500">
												Use {"<, >"} arrow keys to shuffle
											</span>
											<InfoIcon className="w-3 h-3 text-gray-500 cursor-pointer" />
										</div>
									</label>
									<div ref={gradientPresetsRef} className="relative">
										<button
											type="button"
											onClick={() =>
												setIsGradientPresetsOpen(!isGradientPresetsOpen)
											}
											className="w-full h-8 px-2 text-xs border border-zinc-200 bg-white rounded-xl focus:outline-none focus:ring-2 focus:ring-zinc-400 focus:border-zinc-400 transition-colors flex items-center justify-between"
										>
											<span className="text-left">
												{activePreset ? activePreset.name : "Choose Gradient"}
											</span>
											<ChevronDown
												className={`w-3 h-3 transition-transform ${
													isGradientPresetsOpen ? "rotate-180" : ""
												}`}
											/>
										</button>

										<AnimatePresence>
											{isGradientPresetsOpen && (
												<motion.div
													initial={{ opacity: 0, y: -10 }}
													animate={{ opacity: 1, y: 0 }}
													exit={{ opacity: 0, y: -10 }}
													transition={{ duration: 0.15 }}
													className="absolute z-50 w-full mt-1 bg-white border border-zinc-200 rounded-xl shadow-lg overflow-hidden"
													style={{ maxHeight: "400px", overflowY: "auto" }}
												>
													<div className="p-3">
														<div className="grid grid-cols-1 gap-2">
															{gradientPresets.map((preset) => (
																<button
																	key={preset.id}
																	type="button"
																	onClick={() => {
																		setGradient((prev) => ({
																			...prev,
																			type: preset.type,
																			angle: preset.angle,
																			stops: preset.stops.map((stop) => ({
																				...stop,
																			})),
																		}));
																		setIsGradientPresetsOpen(false);
																	}}
																	className="h-10 rounded-xl overflow-hidden hover:border-zinc-400 transition-colors focus:outline-none focus:ring-2 focus:ring-zinc-400"
																	style={{
																		background:
																			generatePresetGradientCSS(preset),
																	}}
																	title={preset.name}
																>
																	<div className="w-full h-full flex items-center justify-center p-1">
																		<span className="text-xs text-white px-1.5 py-0.5 rounded text-left truncate w-full">
																			{preset.name}
																		</span>
																	</div>
																</button>
															))}
														</div>
													</div>
												</motion.div>
											)}
										</AnimatePresence>
									</div>
								</div>
								{/* <div>
									<label className="block text-xs font-medium text-gray-700 mb-1.5">
										Background Patterns
									</label>
									<div ref={backgroundPatternsRef} className="relative">
										<button
											type="button"
											onClick={() =>
												setIsBackgroundPatternsOpen(!isBackgroundPatternsOpen)
											}
											className="w-full h-8 px-2 text-xs border border-zinc-200 bg-white rounded-xl focus:outline-none focus:ring-2 focus:ring-zinc-400 focus:border-zinc-400 transition-colors flex items-center justify-between"
										>
											<span className="text-left">
												{gradient.backgroundPattern.name || "Choose Pattern"}
											</span>
											<ChevronDown
												className={`w-3 h-3 transition-transform ${
													isBackgroundPatternsOpen ? "rotate-180" : ""
												}`}
											/>
										</button>

										<AnimatePresence>
											{isBackgroundPatternsOpen && (
												<motion.div
													initial={{ opacity: 0, y: -10 }}
													animate={{ opacity: 1, y: 0 }}
													exit={{ opacity: 0, y: -10 }}
													transition={{ duration: 0.15 }}
													className="absolute z-50 w-full mt-1 bg-white border border-zinc-200 rounded-xl shadow-lg overflow-hidden"
													style={{ maxHeight: "400px", overflowY: "auto" }}
												>
													<div className="p-3">
														<div className="grid grid-cols-1 gap-2">
															{backgroundPatternPresets.map((pattern) => (
																<button
																	key={pattern.id}
																	type="button"
																	onClick={() => {
																		setGradient((prev) => ({
																			...prev,
																			backgroundPattern: {
																				id: pattern.id,
																				name: pattern.name,
																			},
																		}));
																		setIsBackgroundPatternsOpen(false);
																	}}
																	className={`h-10 rounded-xl overflow-hidden hover:border-zinc-400 transition-colors focus:outline-none focus:ring-2 focus:ring-zinc-400 border ${
																		gradient.backgroundPattern.id === pattern.id
																			? "border-zinc-400"
																			: "border-transparent"
																	}`}
																	style={{
																		background:
																			pattern.preview || "transparent",
																		backgroundSize:
																			pattern.backgroundSize || "auto",
																		backgroundPosition:
																			pattern.backgroundPosition || "0 0",
																	}}
																	title={pattern.name}
																>
																	<div className="w-full h-full flex items-center justify-center p-1 bg-white bg-opacity-50">
																		<span className="text-xs text-gray-900 px-1.5 py-0.5 rounded text-left truncate w-full font-medium">
																			{pattern.name}
																		</span>
																	</div>
																</button>
															))}
														</div>
													</div>
												</motion.div>
											)}
										</AnimatePresence>
									</div>
								</div> */}
								<div>
									<label className="block text-xs font-medium text-gray-700 mb-1.5">
										Gradient Type
									</label>
									<Dropdown
										value={gradient.type}
										onChange={(value) =>
											setGradient((prev) => ({ ...prev, type: value }))
										}
										options={[
											{ value: "linear", label: "Linear" },
											{ value: "radial", label: "Radial" },
											{ value: "conic", label: "Conic" },
											{ value: "mesh", label: "Mesh" },
											{ value: "grid", label: "Grid" },
											{ value: "sharp", label: "Sharp" },
											{ value: "soft", label: "Soft" },
										]}
										placeholder="Select gradient type"
									/>
								</div>

								{/* Noise Controls */}
								<div className="border-t pt-3">
									<h3 className="text-base font-semibold mb-3">Noise</h3>

									<div className="space-y-3">
										<div>
											<label className="flex items-center space-x-2 text-xs font-medium cursor-pointer group mb-3">
												<div className="relative">
													<input
														type="checkbox"
														checked={gradient.noise.enabled}
														onChange={(e) =>
															setGradient((prev) => ({
																...prev,
																noise: {
																	...prev.noise,
																	enabled: e.target.checked,
																},
															}))
														}
														className="sr-only"
													/>
													<div
														className={`w-4 h-4 rounded border-2 transition-all duration-200 flex items-center justify-center ${
															gradient.noise.enabled
																? "bg-zinc-600 border-zinc-600"
																: "bg-white border-zinc-300 group-hover:border-zinc-400"
														}`}
													>
														{gradient.noise.enabled && (
															<svg
																className="w-2.5 h-2.5 text-white"
																fill="none"
																strokeLinecap="round"
																strokeLinejoin="round"
																strokeWidth="2.5"
																viewBox="0 0 24 24"
																stroke="currentColor"
															>
																<path d="M5 13l4 4L19 7"></path>
															</svg>
														)}
													</div>
												</div>
												<span className="select-none">Enable Noise</span>
											</label>
										</div>

										{gradient.noise.enabled && (
											<div>
												<label className="block text-xs font-medium text-gray-700 mb-1.5">
													Intensity:{" "}
													{Math.round(gradient.noise.intensity * 100)}%
												</label>
												<input
													type="range"
													min="0"
													max="1"
													step="0.01"
													value={gradient.noise.intensity}
													onChange={(e) =>
														setGradient((prev) => ({
															...prev,
															noise: {
																...prev.noise,
																intensity: parseFloat(e.target.value),
															},
														}))
													}
													className="w-full h-1.5 bg-zinc-200 rounded-xl appearance-none cursor-pointer slider"
												/>
											</div>
										)}
									</div>
								</div>

								{/* Background Animation Controls */}
								<div className="border-t pt-3">
									<h3 className="text-base font-semibold mb-3">
										Background Animation
									</h3>

									<div className="space-y-3">
										<div>
											<label className="block text-xs font-medium text-gray-700 mb-1.5">
												Animation Type
											</label>
											<Dropdown
												value={gradient.backgroundAnimation.type}
												onChange={(value) =>
													setGradient((prev) => ({
														...prev,
														backgroundAnimation: {
															...prev.backgroundAnimation,
															type: value,
														},
													}))
												}
												options={[
													{ value: "slide", label: "Slide" },
													{ value: "wave", label: "Wave" },
												]}
												placeholder="Select animation type"
											/>
										</div>

										<div>
											<label className="block text-xs font-medium text-gray-700 mb-1.5">
												Direction
											</label>
											<Dropdown
												value={gradient.backgroundAnimation.direction}
												onChange={(value) =>
													setGradient((prev) => ({
														...prev,
														backgroundAnimation: {
															...prev.backgroundAnimation,
															direction: value,
														},
													}))
												}
												options={[
													{ value: "right", label: "Right" },
													{ value: "left", label: "Left" },
													{ value: "up", label: "Up" },
													{ value: "down", label: "Down" },
												]}
												placeholder="Select direction"
											/>
										</div>

										<div>
											<label className="block text-xs font-medium text-gray-700 mb-1.5">
												Speed: {gradient.backgroundAnimation.speed}s
											</label>
											<input
												type="range"
												min="1"
												max="20"
												step="1"
												value={gradient.backgroundAnimation.speed}
												onChange={(e) =>
													setGradient((prev) => ({
														...prev,
														backgroundAnimation: {
															...prev.backgroundAnimation,
															speed: parseInt(e.target.value),
														},
													}))
												}
												className="w-full h-1.5 bg-zinc-200 rounded-xl appearance-none cursor-pointer slider"
											/>
										</div>

										<div>
											<label className="block text-xs font-medium text-gray-700 mb-1.5">
												Easing
											</label>
											<Dropdown
												value={gradient.backgroundAnimation.easing}
												onChange={(value) =>
													setGradient((prev) => ({
														...prev,
														backgroundAnimation: {
															...prev.backgroundAnimation,
															easing: value,
														},
													}))
												}
												options={[
													{ value: "linear", label: "Linear" },
													{ value: "ease", label: "Ease" },
													{ value: "ease-in", label: "Ease In" },
													{ value: "ease-out", label: "Ease Out" },
													{ value: "ease-in-out", label: "Ease In Out" },
												]}
												placeholder="Select easing"
											/>
										</div>

										<div>
											<label className="flex items-center space-x-2 text-xs font-medium cursor-pointer group">
												<div className="relative">
													<input
														type="checkbox"
														checked={gradient.backgroundAnimation.enabled}
														onChange={(e) =>
															setGradient((prev) => ({
																...prev,
																backgroundAnimation: {
																	...prev.backgroundAnimation,
																	enabled: e.target.checked,
																},
															}))
														}
														className="sr-only"
													/>
													<div
														className={`w-4 h-4 rounded border-2 transition-all duration-200 flex items-center justify-center ${
															gradient.backgroundAnimation.enabled
																? "bg-zinc-600 border-zinc-600"
																: "bg-white border-zinc-300 group-hover:border-zinc-400"
														}`}
													>
														{gradient.backgroundAnimation.enabled && (
															<svg
																className="w-2.5 h-2.5 text-white"
																fill="none"
																strokeLinecap="round"
																strokeLinejoin="round"
																strokeWidth="2.5"
																viewBox="0 0 24 24"
																stroke="currentColor"
															>
																<path d="M5 13l4 4L19 7"></path>
															</svg>
														)}
													</div>
												</div>
												<span className="select-none">Enable Animation</span>
											</label>
										</div>

										<div>
											<label className="flex items-center space-x-2 text-xs font-medium cursor-pointer group">
												<div className="relative">
													<input
														type="checkbox"
														checked={
															gradient.backgroundAnimation.repeat || false
														}
														onChange={(e) =>
															setGradient((prev) => ({
																...prev,
																backgroundAnimation: {
																	...prev.backgroundAnimation,
																	repeat: e.target.checked,
																},
															}))
														}
														className="sr-only"
													/>
													<div
														className={`w-4 h-4 rounded border-2 transition-all duration-200 flex items-center justify-center ${
															gradient.backgroundAnimation.repeat
																? "bg-zinc-600 border-zinc-600"
																: "bg-white border-zinc-300 group-hover:border-zinc-400"
														}`}
													>
														{gradient.backgroundAnimation.repeat && (
															<svg
																className="w-2.5 h-2.5 text-white"
																fill="none"
																strokeLinecap="round"
																strokeLinejoin="round"
																strokeWidth="2.5"
																viewBox="0 0 24 24"
																stroke="currentColor"
															>
																<path d="M5 13l4 4L19 7"></path>
															</svg>
														)}
													</div>
												</div>
												<span className="select-none">
													Repeat Animation (infinite)
												</span>
											</label>
										</div>

										{/* Background CSS Output */}
										<div className="border-t pt-3">
											<div className="flex items-center justify-between mb-1.5">
												<h4 className="text-xs font-medium">Background CSS</h4>
												<button
													onClick={() => {
														const patternCSS = generateBackgroundPatternCSS();
														const css = `background: ${generateGradientCSS()};${
															patternCSS ? `\n${patternCSS}` : ""
														}${
															backgroundAnimation
																? `\nbackground-size: 200% 200%;\nanimation: ${backgroundAnimation};`
																: ""
														}`;
														copyToClipboard(css, "background-css");
													}}
													className="flex items-center gap-1 px-1.5 py-0.5 text-[10px] bg-zinc-100 hover:bg-zinc-200 rounded transition-colors"
												>
													<Copy className="w-2.5 h-2.5" />
													{copied === "background-css" ? "Copied!" : "Copy CSS"}
												</button>
											</div>
											<pre className="bg-gray-900 text-zinc-400 p-2 rounded text-[10px] overflow-x-auto max-h-28 overflow-y-auto mb-3">
												<code>{`background: ${generateGradientCSS()};${
													generateBackgroundPatternCSS()
														? `\n${generateBackgroundPatternCSS()}`
														: ""
												}${
													backgroundAnimation
														? `\nbackground-size: 200% 200%;\nanimation: ${backgroundAnimation};`
														: ""
												}`}</code>
											</pre>
											{/* Download Button */}
											<div ref={panelDownloadDropdownRef} className="relative">
												<button
													onClick={() =>
														setIsPanelDownloadDropdownOpen(
															!isPanelDownloadDropdownOpen
														)
													}
													className="w-full flex items-center justify-center gap-2 px-3 py-2 text-xs bg-zinc-800 hover:bg-black hover:shadow-zinc-200 hover:shadow-2xl text-white rounded-xl transition-all duration-75 ease-in hover:scale-105"
												>
													<Download className="w-3 h-3" />
													Download
												</button>
												<AnimatePresence>
													{isPanelDownloadDropdownOpen && (
														<motion.div
															initial={{ opacity: 0, y: -10 }}
															animate={{ opacity: 1, y: 0 }}
															exit={{ opacity: 0, y: -10 }}
															transition={{ duration: 0.15 }}
															className="absolute bottom-full left-0 mb-2 w-full bg-white border border-zinc-200 rounded-xl shadow-lg overflow-hidden z-50"
														>
															<div className="flex flex-col">
																<button
																	type="button"
																	onClick={() => {
																		downloadSVG(previewFrameSize);
																		setIsPanelDownloadDropdownOpen(false);
																	}}
																	className="w-full px-3 py-2 text-xs text-left hover:bg-zinc-50 transition-colors flex items-center gap-2"
																>
																	<Download className="w-3 h-3" />
																	<span>
																		SVG -{" "}
																		{previewFramePresets[previewFrameSize]
																			?.label || "Current Frame"}
																	</span>
																</button>
																<button
																	type="button"
																	onClick={() => {
																		downloadRaster("png", previewFrameSize);
																		setIsPanelDownloadDropdownOpen(false);
																	}}
																	className="w-full px-3 py-2 text-xs text-left hover:bg-zinc-50 transition-colors flex items-center gap-2 border-t border-zinc-100"
																>
																	<Download className="w-3 h-3" />
																	<span>
																		PNG -{" "}
																		{previewFramePresets[previewFrameSize]
																			?.label || "Current Frame"}
																	</span>
																</button>
																<button
																	type="button"
																	onClick={() => {
																		downloadRaster("jpeg", previewFrameSize);
																		setIsPanelDownloadDropdownOpen(false);
																	}}
																	className="w-full px-3 py-2 text-xs text-left hover:bg-zinc-50 transition-colors flex items-center gap-2 border-t border-zinc-100"
																>
																	<Download className="w-3 h-3" />
																	<span>
																		JPEG -{" "}
																		{previewFramePresets[previewFrameSize]
																			?.label || "Current Frame"}
																	</span>
																</button>
																{gradient.backgroundAnimation.enabled && (
																	<button
																		type="button"
																		onClick={() => {
																			downloadGIF(previewFrameSize);
																			setIsPanelDownloadDropdownOpen(false);
																		}}
																		className="w-full px-3 py-2 text-xs text-left hover:bg-zinc-50 transition-colors flex items-center gap-2 border-t border-zinc-100"
																	>
																		<Download className="w-3 h-3" />
																		<span>
																			GIF -{" "}
																			{previewFramePresets[previewFrameSize]
																				?.label || "Current Frame"}
																		</span>
																	</button>
																)}
																<button
																	type="button"
																	onClick={() => {
																		downloadMP4(previewFrameSize);
																		setIsPanelDownloadDropdownOpen(false);
																	}}
																	className="w-full px-3 py-2 text-xs text-left hover:bg-zinc-50 transition-colors flex items-center gap-2 border-t border-zinc-100"
																>
																	<Download className="w-3 h-3" />
																	<span>
																		MP4 -{" "}
																		{previewFramePresets[previewFrameSize]
																			?.label || "Current Frame"}
																	</span>
																</button>
															</div>
														</motion.div>
													)}
												</AnimatePresence>
											</div>
										</div>
									</div>
								</div>
							</div>
						</div>
					</div>
				</div>
			</div>

			{/* Modal */}
			<AnimatePresence>
				{isModalOpen && (
					<motion.div
						initial={{ opacity: 0 }}
						animate={{ opacity: 1 }}
						exit={{ opacity: 0 }}
						className="fixed inset-0 bg-black bg-opacity-80 flex items-center justify-center z-50"
						onClick={() => setIsModalOpen(false)}
					>
						<motion.div
							initial={{ scale: 0.8, opacity: 0 }}
							animate={{ scale: 1, opacity: 1 }}
							exit={{ scale: 0.8, opacity: 0 }}
							className="relative flex items-center justify-center w-full h-full"
							onClick={(e) => e.stopPropagation()}
						>
							{/* Close Button, Download, and Frame Size Selector */}
							<div className="absolute top-4 right-4 flex items-center gap-2 z-50">
								{/* Frame Size Selector */}
								<div className="flex items-center">
									<Dropdown
										value={previewFrameSize}
										onChange={(value) => {
											setPreviewFrameSize(value);
											const frame = previewFramePresets[value];
											if (frame) {
												setGradient((prev) => ({
													...prev,
													dimensions: {
														width: frame.width,
														height: frame.height,
													},
												}));
											}
										}}
										options={Object.entries(previewFramePresets).map(
											([key, preset]) => ({
												value: key,
												label: `${preset.icon} ${preset.label} (${preset.width}×${preset.height})`,
											})
										)}
										placeholder="Select frame size"
										className="min-w-[180px]"
									/>
								</div>
								{/* Download Button with Dropdown */}
								<div ref={downloadDropdownRef} className="relative">
									<button
										onClick={() =>
											setIsDownloadDropdownOpen(!isDownloadDropdownOpen)
										}
										className="flex items-center gap-2 px-3 py-1.5 text-sm bg-white hover:bg-gray-100 rounded-xl transition-colors border border-gray-200 shadow-lg h-10"
									>
										<Download className="w-4 h-4" />
										Download
									</button>
									<AnimatePresence>
										{isDownloadDropdownOpen && (
											<motion.div
												initial={{ opacity: 0, y: -10 }}
												animate={{ opacity: 1, y: 0 }}
												exit={{ opacity: 0, y: -10 }}
												transition={{ duration: 0.15 }}
												className="absolute right-0 mt-2 bg-white border border-gray-200 rounded-xl shadow-lg overflow-hidden z-50 min-w-[200px]"
											>
												<div className="flex flex-col">
													<button
														type="button"
														onClick={() => {
															downloadSVG(previewFrameSize);
															setIsDownloadDropdownOpen(false);
														}}
														className="w-full px-4 py-3 text-sm text-left hover:bg-gray-50 transition-colors flex items-center gap-2"
													>
														<Download className="w-4 h-4" />
														<span>
															Download as SVG -{" "}
															{previewFramePresets[previewFrameSize]?.label ||
																"Current Frame"}
														</span>
													</button>
													<button
														type="button"
														onClick={() => {
															downloadRaster("png", previewFrameSize);
															setIsDownloadDropdownOpen(false);
														}}
														className="w-full px-4 py-3 text-sm text-left hover:bg-gray-50 transition-colors flex items-center gap-2 border-t border-gray-100"
													>
														<Download className="w-4 h-4" />
														<span>
															Download as PNG -{" "}
															{previewFramePresets[previewFrameSize]?.label ||
																"Current Frame"}
														</span>
													</button>
													<button
														type="button"
														onClick={() => {
															downloadRaster("jpeg", previewFrameSize);
															setIsDownloadDropdownOpen(false);
														}}
														className="w-full px-4 py-3 text-sm text-left hover:bg-gray-50 transition-colors flex items-center gap-2 border-t border-gray-100"
													>
														<Download className="w-4 h-4" />
														<span>
															Download as JPEG -{" "}
															{previewFramePresets[previewFrameSize]?.label ||
																"Current Frame"}
														</span>
													</button>
													{/* GIF Download - Only show if background animation is enabled */}
													{gradient.backgroundAnimation.enabled && (
														<button
															type="button"
															onClick={() => {
																downloadGIF(previewFrameSize);
																setIsDownloadDropdownOpen(false);
															}}
															className="w-full px-4 py-3 text-sm text-left hover:bg-gray-50 transition-colors flex items-center gap-2 border-t border-gray-100"
														>
															<Download className="w-4 h-4" />
															<span>
																Download as GIF -{" "}
																{previewFramePresets[previewFrameSize]?.label ||
																	"Current Frame"}
															</span>
														</button>
													)}
													{/* MP4 Download */}
													<button
														type="button"
														onClick={() => {
															downloadMP4(previewFrameSize);
															setIsDownloadDropdownOpen(false);
														}}
														className="w-full px-4 py-3 text-sm text-left hover:bg-gray-50 transition-colors flex items-center gap-2 border-t border-gray-100"
													>
														<Download className="w-4 h-4" />
														<span>
															Download as MP4 -{" "}
															{previewFramePresets[previewFrameSize]?.label ||
																"Current Frame"}
														</span>
													</button>
												</div>
											</motion.div>
										)}
									</AnimatePresence>
								</div>
								{/* Close Button */}
								<button
									onClick={() => setIsModalOpen(false)}
									className="w-10 h-10 bg-white hover:bg-gray-100 rounded-full flex items-center justify-center shadow-lg transition-colors border border-gray-200"
								>
									<X className="w-5 h-5 text-gray-900" />
								</button>
							</div>

							{/* Modal Preview Container */}
							<div
								ref={modalPreviewRef}
								className="relative rounded-xl overflow-hidden shadow-2xl"
								style={{
									aspectRatio: `${gradient.dimensions.width} / ${gradient.dimensions.height}`,
									...modalDimensions,
									background: generateGradientCSS(),
									...(isPlaying &&
										gradient.backgroundAnimation.enabled && {
											backgroundSize: "200% 200%",
										}),
									...(isPlaying &&
										backgroundAnimation && {
											animation: backgroundAnimation,
										}),
								}}
							/>
						</motion.div>
					</motion.div>
				)}
			</AnimatePresence>

			{/* MP4 Generation Loading Modal */}
			<AnimatePresence>
				{isGeneratingMP4 && (
					<motion.div
						initial={{ opacity: 0 }}
						animate={{ opacity: 1 }}
						exit={{ opacity: 0 }}
						className="fixed inset-0 bg-black bg-opacity-80 flex items-center justify-center z-50"
					>
						<motion.div
							initial={{ scale: 0.8, opacity: 0 }}
							animate={{ scale: 1, opacity: 1 }}
							exit={{ scale: 0.8, opacity: 0 }}
							className="bg-white rounded-xl p-8 max-w-md w-full mx-4"
						>
							<div className="text-center">
								<div className="mb-4">
									<div className="inline-block animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-purple-600"></div>
								</div>
								<h3 className="text-xl font-semibold mb-2">
									Generating MP4 Video
								</h3>
								<p className="text-gray-600 mb-4">
									{mp4Progress < 20
										? "Initializing..."
										: mp4Progress < 70
											? "Recording frames..."
											: mp4Progress < 90
												? "Converting to MP4..."
												: "Finalizing..."}
								</p>
								<div className="w-full bg-gray-200 rounded-full h-2.5 mb-2">
									<div
										className="bg-purple-600 h-2.5 rounded-full transition-all duration-300"
										style={{ width: `${mp4Progress}%` }}
									></div>
								</div>
								<p className="text-sm text-gray-500">
									{Math.round(mp4Progress)}%
								</p>
							</div>
						</motion.div>
					</motion.div>
				)}
			</AnimatePresence>
		</div>
	);
};

export default GradientGenerator;

The Tech stack I am using is as follows

  1. Nextjs
  2. Reactjs
  3. Tailwind CSS
  4. Lucide React for icons
  5. Framer motion for animation 

The whole idea is to quickly make something that I can use and launch it as a URL, so I didn't pay attention to user authentication, CRUD operations by user, etc, and only one feature is added is to download the output.

One can look, I've added gradient presets to further allow people to quickly iterate over examples because I hate doing manual work every time, and templates save time. I'll add more gradients, almost 100,+ but I hope that won't confuse the user, so currently it only has 10/20 presets as examples.

That's the story for today, see you in the next one

Shrey

External Links (3)

Explore topics

More in

Weekly letter

Subscribe to iHateReading

Our once-a-week newsletter on programming, jobs, AI, and building products online.

Our once a week newsletter on Programming, Jobs, AI, and Business