A color picker component that allows users to select and customize colors with various formats and options..
1"use client"
2
3import React, { useCallback, useState } from "react"
4import { Check, Copy, Lock, LockOpen, Palette, RefreshCw } from "lucide-react"
5import { motion } from "motion/react"
6import { Poline, positionFunctions } from "poline"
7
8import { Button } from "@/components/ui/button"
9import { Card, CardContent } from "@/components/ui/card"
10import {
11 Tooltip,
12 TooltipContent,
13 TooltipProvider,
14 TooltipTrigger,
15} from "@/components/ui/tooltip"
16
17import ColorPicker from "../ui/color-picker"
18
19type ColorScheme = {
20 [key: string]: string
21}
22
23export function ColorPickerDemo() {
24 const [colorScheme, setColorScheme] = useState<ColorScheme>({
25 background: "0 0% 100%",
26 foreground: "240 10% 3.9%",
27 card: "0 0% 100%",
28 "card-foreground": "240 10% 3.9%",
29 popover: "0 0% 100%",
30 "popover-foreground": "240 10% 3.9%",
31 primary: "240 5.9% 10%",
32 "primary-foreground": "0 0% 98%",
33 secondary: "240 4.8% 95.9%",
34 "secondary-foreground": "240 5.9% 10%",
35 muted: "240 4.8% 95.9%",
36 "muted-foreground": "240 3.8% 46.1%",
37 accent: "240 4.8% 95.9%",
38 "accent-foreground": "240 5.9% 10%",
39 destructive: "0 84.2% 60.2%",
40 "destructive-foreground": "0 0% 98%",
41 border: "240 5.9% 90%",
42 input: "240 5.9% 90%",
43 ring: "240 5.9% 10%",
44 })
45 const [lockedColor, setLockedColor] = useState<string | null>(null)
46 const [copied, setCopied] = useState(false)
47
48 const generateHarmoniousColors = useCallback(() => {
49 let anchorColors: [number, number, number][] = []
50
51 if (lockedColor) {
52 const [h, s, l] = colorScheme[lockedColor].split(" ").map(parseFloat)
53 anchorColors.push([h, s / 100, l / 100])
54 }
55
56 while (anchorColors.length < 3) {
57 anchorColors.push([Math.random() * 360, 0.7, 0.5])
58 }
59
60 const poline = new Poline({
61 numPoints: 20,
62 anchorColors,
63 positionFunctionX: positionFunctions.sinusoidalPosition,
64 positionFunctionY: positionFunctions.quadraticPosition,
65 positionFunctionZ: positionFunctions.linearPosition,
66 })
67
68 const newColorScheme = { ...colorScheme }
69 const colors = poline.colorsCSS
70
71 Object.keys(newColorScheme).forEach((key, index) => {
72 if (key !== lockedColor) {
73 const color = colors[index % colors.length]
74 const [h, s, l] = color.match(/\d+(\.\d+)?/g)?.map(Number) || [0, 0, 0]
75
76 let adjustedLightness = l
77 if (key.includes("foreground")) {
78 adjustedLightness = Math.min(l - 30, 20)
79 } else if (key === "background") {
80 adjustedLightness = Math.max(l + 30, 90)
81 } else if (key === "border" || key === "input") {
82 adjustedLightness = Math.min(Math.max(l, 70), 90)
83 }
84
85 newColorScheme[key] = `${h.toFixed(1)} ${s.toFixed(
86 1
87 )}% ${adjustedLightness.toFixed(1)}%`
88 }
89 })
90
91 setColorScheme(newColorScheme)
92 }, [colorScheme, lockedColor])
93
94 const resetColors = useCallback(() => {
95 setColorScheme({
96 background: "0 0% 100%",
97 foreground: "240 10% 3.9%",
98 card: "0 0% 100%",
99 "card-foreground": "240 10% 3.9%",
100 popover: "0 0% 100%",
101 "popover-foreground": "240 10% 3.9%",
102 primary: "240 5.9% 10%",
103 "primary-foreground": "0 0% 98%",
104 secondary: "240 4.8% 95.9%",
105 "secondary-foreground": "240 5.9% 10%",
106 muted: "240 4.8% 95.9%",
107 "muted-foreground": "240 3.8% 46.1%",
108 accent: "240 4.8% 95.9%",
109 "accent-foreground": "240 5.9% 10%",
110 destructive: "0 84.2% 60.2%",
111 "destructive-foreground": "0 0% 98%",
112 border: "240 5.9% 90%",
113 input: "240 5.9% 90%",
114 ring: "240 5.9% 10%",
115 })
116 setLockedColor(null)
117 }, [])
118
119 const copyColorScheme = useCallback(() => {
120 const cssVariables = Object.entries(colorScheme)
121 .map(([key, value]) => `--${key}: ${value};`)
122 .join("\n ")
123
124 const fullCss = `@layer base {
125 :root {
126 ${cssVariables}
127 }
128}`
129
130 navigator.clipboard.writeText(fullCss)
131 setCopied(true)
132 setTimeout(() => setCopied(false), 2000)
133 }, [colorScheme])
134
135 const getContrastColor = useCallback((color: string) => {
136 const [, , lightness] = color.split(" ").map(parseFloat)
137 return lightness > 50 ? "0 0% 0%" : "0 0% 100%"
138 }, [])
139
140 const toggleLock = useCallback((key: string) => {
141 setLockedColor((prev) => (prev === key ? null : key))
142 }, [])
143
144 return (
145 <div className="w-full max-w-4xl mx-auto ">
146 <CardContent className="p-6 space-y-6">
147 <div className="grid md:grid-cols-1 gap-6">
148 <div className="space-y-4">
149 <div className="flex flex-col md:flex-row gap-4 md:justify-between">
150 <Button
151 variant="outline"
152 onClick={generateHarmoniousColors}
153 className="text-sm"
154 >
155 Generate Harmonious Colors
156 </Button>
157 <Button
158 variant="outline"
159 onClick={resetColors}
160 className="text-sm"
161 >
162 <RefreshCw className="w-4 h-4 mr-2" />
163 Reset Colors
164 </Button>
165 </div>
166 <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
167 {Object.entries(colorScheme).map(([key, value]) => (
168 <div key={key} className="relative">
169 <div className="flex items-center justify-between">
170 <label className="text-sm font-medium text-muted-foreground mb-2 block">
171 {key}
172 </label>
173 <Button
174 variant="ghost"
175 size="icon"
176 className="ml-2"
177 onClick={() => toggleLock(key)}
178 >
179 {lockedColor === key ? (
180 <Lock className="h-4 w-4" />
181 ) : (
182 <LockOpen className="h-4 w-4" />
183 )}
184 </Button>
185 </div>
186 <div className="flex items-center">
187 <ColorPicker
188 color={`hsl(${value})`}
189 onChange={(newColor) => {
190 const [h, s, l] = newColor
191 .match(/\d+(\.\d+)?/g)
192 ?.map(Number) || [0, 0, 0]
193 setColorScheme({
194 ...colorScheme,
195 [key]: `${h.toFixed(1)} ${s.toFixed(1)}% ${l.toFixed(
196 1
197 )}%`,
198 })
199 }}
200 />
201 </div>
202 </div>
203 ))}
204 </div>
205 </div>
206 <motion.div
207 className="w-full h-full min-h-[24rem] rounded-lg p-6 shadow-lg transition-colors duration-300 ease-in-out overflow-hidden"
208 style={{
209 backgroundColor: `hsl(${colorScheme.background})`,
210 color: `hsl(${colorScheme.foreground})`,
211 borderColor: `hsl(${colorScheme.border})`,
212 borderWidth: 2,
213 borderStyle: "solid",
214 }}
215 initial={{ opacity: 0, y: 20 }}
216 animate={{ opacity: 1, y: 0 }}
217 transition={{ duration: 0.5 }}
218 >
219 <h3 className="text-xl font-semibold mb-4">Color Preview</h3>
220 <p className="text-sm mb-4">
221 Experience your color palette in action. This preview showcases
222 your selected colors.
223 </p>
224 <div className="space-y-2">
225 {Object.entries(colorScheme).map(([key, value]) => (
226 <div
227 key={key}
228 className="flex flex-col md:flex-row gap-4 md:items-center justify-between"
229 >
230 <span>{key}</span>
231 <TooltipProvider>
232 <Tooltip>
233 <TooltipTrigger asChild>
234 <Button
235 variant="outline"
236 size="sm"
237 className="font-mono"
238 onClick={() => {
239 navigator.clipboard.writeText(`--${key}: ${value};`)
240 setCopied(true)
241 setTimeout(() => setCopied(false), 2000)
242 }}
243 style={{
244 backgroundColor: `hsl(${value})`,
245 color: `hsl(${getContrastColor(value)})`,
246 borderColor: `hsl(${colorScheme.border})`,
247 }}
248 >
249 {value}
250 {copied ? (
251 <Check className="w-4 h-4 ml-2" />
252 ) : (
253 <Copy className="w-4 h-4 ml-2" />
254 )}
255 </Button>
256 </TooltipTrigger>
257 <TooltipContent>
258 <p>Click to copy</p>
259 </TooltipContent>
260 </Tooltip>
261 </TooltipProvider>
262 </div>
263 ))}
264 </div>
265 </motion.div>
266 <Button onClick={copyColorScheme} className="w-full">
267 Copy Full Color Scheme
268 </Button>
269 </div>
270 </CardContent>
271 </div>
272 )
273}