{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "color-picker-input", "type": "registry:ui", "title": "Color Picker Input", "description": "iOS 26 liquid-glass color picker with swatch grid, draggable opacity slider, and one-click copy.", "dependencies": [ "motion" ], "files": [ { "path": "registry/ruixenui/color-picker-input.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { motion, AnimatePresence } from \"motion/react\";\n\n/* ── sound ── */\nlet _a: AudioContext, _b: AudioBuffer;\nconst tick = () => {\n if (typeof window === \"undefined\") return;\n if (!_a) {\n _a = new AudioContext();\n _b = _a.createBuffer(1, (_a.sampleRate * 0.003) | 0, _a.sampleRate);\n const d = _b.getChannelData(0);\n for (let i = 0; i < d.length; i++)\n d[i] = (Math.random() * 2 - 1) * (1 - i / d.length) ** 4;\n }\n const s = _a.createBufferSource();\n s.buffer = _b;\n const g = _a.createGain();\n g.gain.value = 0.08;\n s.connect(g).connect(_a.destination);\n s.start();\n};\n\n/* ── theme ── */\nconst CSS = `\n.cp{\n --cp-glass:linear-gradient(180deg,rgba(255,255,255,0.78),rgba(255,255,255,0.62));\n --cp-border:rgba(0,0,0,0.06);\n --cp-shadow:0 0 1px rgba(0,0,0,0.04),0 2px 8px rgba(0,0,0,0.04),inset 0 1px 0 rgba(255,255,255,0.8);\n --cp-dim:rgba(0,0,0,0.42);\n --cp-mid:rgba(0,0,0,0.55);\n --cp-hi:rgba(0,0,0,0.88);\n --cp-sep:rgba(0,0,0,0.06);\n --cp-track:rgba(0,0,0,0.06);\n --cp-knob:#fff;\n --cp-knob-border:rgba(0,0,0,0.08);\n --cp-knob-shadow:0 1px 3px rgba(0,0,0,0.15);\n --cp-btn:rgba(0,0,0,0.03);\n --cp-btn-h:rgba(0,0,0,0.07);\n --cp-ok:rgba(34,197,94,0.8)\n}\n.dark .cp,[data-theme=\"dark\"] .cp{\n --cp-glass:linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02));\n --cp-border:rgba(255,255,255,0.07);\n --cp-shadow:0 1px 3px rgba(0,0,0,0.08),inset 0 1px 0 rgba(255,255,255,0.04);\n --cp-dim:rgba(255,255,255,0.28);\n --cp-mid:rgba(255,255,255,0.5);\n --cp-hi:rgba(255,255,255,0.88);\n --cp-sep:rgba(255,255,255,0.06);\n --cp-track:rgba(255,255,255,0.06);\n --cp-knob:#fff;\n --cp-knob-border:rgba(255,255,255,0.06);\n --cp-knob-shadow:0 1px 3px rgba(0,0,0,0.2);\n --cp-btn:rgba(255,255,255,0.03);\n --cp-btn-h:rgba(255,255,255,0.07);\n --cp-ok:rgba(74,222,128,0.9)\n}`;\n\nconst MONO =\n \"ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace\";\n\nconst DEFAULT_COLORS = [\n \"#F87171\",\n \"#FB923C\",\n \"#FACC15\",\n \"#4ADE80\",\n \"#22D3EE\",\n \"#3B82F6\",\n \"#8B5CF6\",\n \"#EC4899\",\n \"#A3E635\",\n \"#06B6D4\",\n \"#0EA5E9\",\n \"#6366F1\",\n];\n\nconst toRGBA = (hex: string, opacity: number) => {\n const alpha = Math.round(opacity * 2.55)\n .toString(16)\n .padStart(2, \"0\");\n return `${hex}${alpha}`;\n};\n\n/* ── types ── */\ninterface ColorPickerInputProps {\n colors?: string[];\n initialColor?: string;\n initialOpacity?: number;\n onChange?: (color: string, opacity: number) => void;\n sound?: boolean;\n style?: React.CSSProperties;\n}\n\n/* ── component ── */\nexport default function ColorPickerInput({\n colors = DEFAULT_COLORS,\n initialColor = \"#3B82F6\",\n initialOpacity = 100,\n onChange,\n sound = true,\n style,\n}: ColorPickerInputProps) {\n const [color, setColor] = React.useState(initialColor);\n const [opacity, setOpacity] = React.useState(initialOpacity);\n const [copied, setCopied] = React.useState(false);\n const sliderRef = React.useRef(null);\n const dragging = React.useRef(false);\n\n const finalColor = toRGBA(color, opacity);\n\n const selectColor = (c: string) => {\n setColor(c);\n if (sound) tick();\n onChange?.(c, opacity);\n };\n\n const updateOpacity = (e: PointerEvent | React.PointerEvent) => {\n const el = sliderRef.current;\n if (!el) return;\n const rect = el.getBoundingClientRect();\n const pct = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));\n const val = Math.round(pct * 100);\n setOpacity(val);\n onChange?.(color, val);\n };\n\n const onSliderDown = (e: React.PointerEvent) => {\n e.currentTarget.setPointerCapture(e.pointerId);\n dragging.current = true;\n updateOpacity(e);\n };\n\n const onSliderMove = (e: React.PointerEvent) => {\n if (!dragging.current) return;\n updateOpacity(e);\n };\n\n const onSliderUp = () => {\n dragging.current = false;\n if (sound) tick();\n };\n\n const handleCopy = async () => {\n try {\n await navigator.clipboard.writeText(finalColor.toUpperCase());\n if (sound) tick();\n setCopied(true);\n setTimeout(() => setCopied(false), 1500);\n } catch {\n /* silent */\n }\n };\n\n return (\n <>\n