"use client"; import * as React from "react"; import { cn } from "../../lib/utils"; import { Button } from "./button"; import { Input } from "./input"; import { Label } from "./label"; import { Popover, PopoverContent, PopoverTrigger, } from "./popover"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "./tabs"; import { Slider } from "./slider"; import { cva, type VariantProps } from "class-variance-authority"; import { motion, AnimatePresence } from "framer-motion"; import { Copy, Check, Palette, Pipette } from "lucide-react"; /** * ColorPicker Component * * A comprehensive color picker with multiple input methods */ const colorPickerVariants = cva( "relative inline-flex items-center justify-center", { variants: { size: { default: "h-10 w-10", sm: "h-8 w-8", lg: "h-12 w-12", xl: "h-16 w-16", }, shape: { square: "rounded-md", circle: "rounded-full", rounded: "rounded-lg", }, }, defaultVariants: { size: "default", shape: "square", }, } ); // Color utilities function hexToRgb(hex: string): { r: number; g: number; b: number } | null { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16), } : null; } function rgbToHex(r: number, g: number, b: number): string { return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); } function hexToHsl(hex: string): { h: number; s: number; l: number } | null { const rgb = hexToRgb(hex); if (!rgb) return null; const r = rgb.r / 255; const g = rgb.g / 255; const b = rgb.b / 255; const max = Math.max(r, g, b); const min = Math.min(r, g, b); let h = 0; let s = 0; const l = (max + min) / 2; if (max !== min) { const d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min); switch (max) { case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break; case g: h = ((b - r) / d + 2) / 6; break; case b: h = ((r - g) / d + 4) / 6; break; } } return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100), }; } function hslToHex(h: number, s: number, l: number): string { h = h / 360; s = s / 100; l = l / 100; let r, g, b; if (s === 0) { r = g = b = l; } else { const hue2rgb = (p: number, q: number, t: number) => { if (t < 0) t += 1; if (t > 1) t -= 1; if (t < 1 / 6) return p + (q - p) * 6 * t; if (t < 1 / 2) return q; if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; return p; }; const q = l < 0.5 ? l * (1 + s) : l + s - l * s; const p = 2 * l - q; r = hue2rgb(p, q, h + 1 / 3); g = hue2rgb(p, q, h); b = hue2rgb(p, q, h - 1 / 3); } return rgbToHex( Math.round(r * 255), Math.round(g * 255), Math.round(b * 255) ); } export interface ColorPickerProps extends Omit, "onChange" | "value">, VariantProps { /** * Current color value */ value?: string; /** * Callback when color changes */ onChange?: (color: string) => void; /** * Show color input */ showInput?: boolean; /** * Show preset colors */ showPresets?: boolean; /** * Preset colors */ presets?: string[]; /** * Allow opacity */ allowOpacity?: boolean; /** * Color format */ format?: "hex" | "rgb" | "hsl"; /** * Disable copy button */ disableCopy?: boolean; /** * Trigger mode */ triggerMode?: "click" | "hover"; } // Default preset colors const defaultPresets = [ "#000000", "#ffffff", "#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff", "#00ffff", "#ff8800", "#8800ff", "#88ff00", "#0088ff", "#ff0088", "#00ff88", "#888888", "#f87171", "#fb923c", "#fbbf24", "#facc15", "#a3e635", "#4ade80", "#34d399", "#2dd4bf", "#22d3ee", "#38bdf8", "#60a5fa", "#818cf8", "#a78bfa", "#c084fc", "#e879f9", "#f472b6", "#fb7185", "#f43f5e", "#ef4444", "#dc2626", ]; export function ColorPicker({ className, size, shape, value = "#000000", onChange, showInput = true, showPresets = true, presets = defaultPresets, allowOpacity = false, format = "hex", disableCopy = false, triggerMode = "click", disabled, ...props }: ColorPickerProps) { const [open, setOpen] = React.useState(false); const [color, setColor] = React.useState(value); const [opacity, setOpacity] = React.useState(100); const [copied, setCopied] = React.useState(false); const [inputValue, setInputValue] = React.useState(value); // Color components const hsl = React.useMemo(() => hexToHsl(color) || { h: 0, s: 0, l: 0 }, [color]); const rgb = React.useMemo(() => hexToRgb(color) || { r: 0, g: 0, b: 0 }, [color]); React.useEffect(() => { setColor(value); setInputValue(value); }, [value]); const handleColorChange = (newColor: string) => { setColor(newColor); setInputValue(newColor); onChange?.(newColor); }; const handleHslChange = (component: "h" | "s" | "l", value: number) => { const newHsl = { ...hsl, [component]: value }; const newColor = hslToHex(newHsl.h, newHsl.s, newHsl.l); handleColorChange(newColor); }; const handleRgbChange = (component: "r" | "g" | "b", value: number) => { const newRgb = { ...rgb, [component]: value }; const newColor = rgbToHex(newRgb.r, newRgb.g, newRgb.b); handleColorChange(newColor); }; const handleInputChange = (e: React.ChangeEvent) => { const newValue = e.target.value; setInputValue(newValue); if (/^#[0-9A-F]{6}$/i.test(newValue)) { handleColorChange(newValue); } }; const copyToClipboard = async () => { try { let textToCopy = color; if (format === "rgb") { textToCopy = `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`; } else if (format === "hsl") { textToCopy = `hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)`; } await navigator.clipboard.writeText(textToCopy); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch (err) { console.error("Failed to copy:", err); } }; const formatDisplay = () => { if (format === "rgb") return `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`; if (format === "hsl") return `hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)`; return color; }; return ( Picker Sliders Input {/* Color gradient picker */}
{/* Hue slider */}
handleHslChange("h", value)} max={360} step={1} className="absolute inset-0" />
{/* Opacity slider */} {allowOpacity && (
setOpacity(value)} max={100} step={1} className="flex-1" /> {opacity}%
)} {/* RGB Sliders */}
{rgb.r}
handleRgbChange("r", value)} max={255} step={1} className="[&_[role=slider]]:bg-red-500" />
{rgb.g}
handleRgbChange("g", value)} max={255} step={1} className="[&_[role=slider]]:bg-green-500" />
{rgb.b}
handleRgbChange("b", value)} max={255} step={1} className="[&_[role=slider]]:bg-blue-500" />
{/* HSL Info */}

H

{hsl.h}°

S

{hsl.s}%

L

{hsl.l}%

handleRgbChange("r", parseInt(e.target.value) || 0)} min={0} max={255} className="text-center" /> handleRgbChange("g", parseInt(e.target.value) || 0)} min={0} max={255} className="text-center" /> handleRgbChange("b", parseInt(e.target.value) || 0)} min={0} max={255} className="text-center" />
{/* Color preview and copy */}

{formatDisplay()}

Current color

{!disableCopy && ( )}
{/* Preset colors */} {showPresets && presets.length > 0 && (
{presets.map((presetColor) => (
)} ); } /** * SimpleColorPicker Component * * A simplified color picker with preset colors only */ export interface SimpleColorPickerProps { value?: string; onChange?: (color: string) => void; colors?: string[]; className?: string; size?: "sm" | "default" | "lg"; } export function SimpleColorPicker({ value = "#000000", onChange, colors = defaultPresets.slice(0, 10), className, size = "default", }: SimpleColorPickerProps) { const sizeClasses = { sm: "h-6 w-6", default: "h-8 w-8", lg: "h-10 w-10", }; return (
{colors.map((color) => (
); } /** * GradientPicker Component * * Pick gradient colors */ export interface GradientPickerProps { value?: { start: string; end: string; angle?: number }; onChange?: (gradient: { start: string; end: string; angle: number }) => void; className?: string; } export function GradientPicker({ value = { start: "#ff0000", end: "#0000ff", angle: 90 }, onChange, className, }: GradientPickerProps) { const [gradient, setGradient] = React.useState(value); const handleChange = (field: "start" | "end" | "angle", newValue: string | number) => { const newGradient = { ...gradient, [field]: newValue }; setGradient(newGradient); onChange?.(newGradient as { start: string; end: string; angle: number }); }; return (
handleChange("start", color)} size="sm" />
handleChange("end", color)} size="sm" />
{gradient.angle}°
handleChange("angle", value)} max={360} step={1} />
); } // Re-export icons used export { Palette, Pipette } from "lucide-react";