{
  "$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<HTMLDivElement>(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      <style dangerouslySetInnerHTML={{ __html: CSS }} />\n      <motion.div\n        className=\"cp\"\n        initial={{ opacity: 0, y: 8 }}\n        animate={{ opacity: 1, y: 0 }}\n        transition={{ duration: 0.35, ease: [0.25, 0.1, 0.25, 1] }}\n        style={{\n          display: \"inline-flex\",\n          flexDirection: \"column\",\n          borderRadius: 14,\n          background: \"var(--cp-glass)\",\n          border: \"1px solid var(--cp-border)\",\n          boxShadow: \"var(--cp-shadow)\",\n          backdropFilter: \"blur(24px)\",\n          WebkitBackdropFilter: \"blur(24px)\",\n          overflow: \"hidden\",\n          width: 300,\n          ...style,\n        }}\n      >\n        {/* header */}\n        <div\n          style={{\n            display: \"flex\",\n            alignItems: \"center\",\n            justifyContent: \"space-between\",\n            padding: \"14px 16px\",\n          }}\n        >\n          <span\n            style={{ fontSize: 13, fontWeight: 500, color: \"var(--cp-hi)\" }}\n          >\n            Color\n          </span>\n          <div\n            style={{\n              width: 22,\n              height: 22,\n              borderRadius: 11,\n              border: \"1px solid var(--cp-border)\",\n              backgroundColor: finalColor,\n              transition: \"background-color 0.2s\",\n            }}\n          />\n        </div>\n\n        <div style={{ height: 1, background: \"var(--cp-sep)\" }} />\n\n        {/* swatches */}\n        <div\n          style={{\n            display: \"grid\",\n            gridTemplateColumns: \"repeat(6, 1fr)\",\n            gap: 8,\n            padding: \"14px 16px\",\n          }}\n        >\n          {colors.map((c) => (\n            <motion.button\n              key={c}\n              onClick={() => selectColor(c)}\n              whileTap={{ scale: 0.85 }}\n              style={{\n                width: 32,\n                height: 32,\n                borderRadius: 16,\n                backgroundColor: c,\n                border: \"none\",\n                cursor: \"pointer\",\n                position: \"relative\",\n                display: \"flex\",\n                alignItems: \"center\",\n                justifyContent: \"center\",\n              }}\n            >\n              <AnimatePresence>\n                {color === c && (\n                  <motion.svg\n                    key=\"check\"\n                    initial={{ opacity: 0, scale: 0 }}\n                    animate={{ opacity: 1, scale: 1 }}\n                    exit={{ opacity: 0, scale: 0 }}\n                    transition={{ type: \"spring\", stiffness: 500, damping: 30 }}\n                    width={14}\n                    height={14}\n                    viewBox=\"0 0 16 16\"\n                    fill=\"none\"\n                    stroke=\"#fff\"\n                    strokeWidth={2.5}\n                    strokeLinecap=\"round\"\n                    strokeLinejoin=\"round\"\n                    style={{ filter: \"drop-shadow(0 1px 2px rgba(0,0,0,0.3))\" }}\n                  >\n                    <path d=\"M3 8.5l3.5 3.5L13 5\" />\n                  </motion.svg>\n                )}\n              </AnimatePresence>\n            </motion.button>\n          ))}\n        </div>\n\n        <div style={{ height: 1, background: \"var(--cp-sep)\" }} />\n\n        {/* opacity slider */}\n        <div style={{ padding: \"14px 16px\" }}>\n          <div\n            style={{\n              display: \"flex\",\n              justifyContent: \"space-between\",\n              fontSize: 11,\n              fontWeight: 500,\n              color: \"var(--cp-dim)\",\n              marginBottom: 10,\n            }}\n          >\n            <span>Opacity</span>\n            <span style={{ fontVariantNumeric: \"tabular-nums\" }}>\n              {opacity}%\n            </span>\n          </div>\n          <div\n            ref={sliderRef}\n            onPointerDown={onSliderDown}\n            onPointerMove={onSliderMove}\n            onPointerUp={onSliderUp}\n            style={{\n              position: \"relative\",\n              height: 6,\n              borderRadius: 3,\n              background: \"var(--cp-track)\",\n              cursor: \"pointer\",\n              touchAction: \"none\" as const,\n            }}\n          >\n            <div\n              style={{\n                position: \"absolute\",\n                left: 0,\n                top: 0,\n                height: \"100%\",\n                width: `${opacity}%`,\n                borderRadius: 3,\n                backgroundColor: color,\n                opacity: 0.6,\n                transition: dragging.current ? \"none\" : \"width 0.1s\",\n              }}\n            />\n            <motion.div\n              style={{\n                position: \"absolute\",\n                top: \"50%\",\n                left: `${opacity}%`,\n                transform: \"translate(-50%, -50%)\",\n                width: 16,\n                height: 16,\n                borderRadius: 8,\n                background: \"var(--cp-knob)\",\n                border: \"1px solid var(--cp-knob-border)\",\n                boxShadow: \"var(--cp-knob-shadow)\",\n              }}\n              whileTap={{ scale: 1.15 }}\n            />\n          </div>\n        </div>\n\n        <div style={{ height: 1, background: \"var(--cp-sep)\" }} />\n\n        {/* output + copy */}\n        <div\n          style={{\n            display: \"flex\",\n            alignItems: \"center\",\n            padding: \"10px 16px\",\n            gap: 8,\n          }}\n        >\n          <span\n            style={{\n              flex: 1,\n              fontSize: 12,\n              fontFamily: MONO,\n              fontVariantNumeric: \"tabular-nums\",\n              color: \"var(--cp-hi)\",\n              userSelect: \"all\" as const,\n            }}\n          >\n            {finalColor.toUpperCase()}\n          </span>\n          <motion.button\n            onClick={handleCopy}\n            whileTap={{ scale: 0.85 }}\n            style={{\n              width: 30,\n              height: 30,\n              borderRadius: 8,\n              border: \"none\",\n              background: \"var(--cp-btn)\",\n              cursor: \"pointer\",\n              display: \"flex\",\n              alignItems: \"center\",\n              justifyContent: \"center\",\n              color: copied ? \"var(--cp-ok)\" : \"var(--cp-mid)\",\n              transition: \"background 0.15s, color 0.2s\",\n            }}\n            onMouseEnter={(e) => {\n              if (!copied)\n                (e.currentTarget as HTMLElement).style.background =\n                  \"var(--cp-btn-h)\";\n            }}\n            onMouseLeave={(e) => {\n              (e.currentTarget as HTMLElement).style.background =\n                \"var(--cp-btn)\";\n            }}\n          >\n            <AnimatePresence mode=\"wait\">\n              {copied ? (\n                <motion.svg\n                  key=\"check\"\n                  initial={{ opacity: 0, scale: 0.5 }}\n                  animate={{ opacity: 1, scale: 1 }}\n                  exit={{ opacity: 0, scale: 0.5 }}\n                  transition={{ type: \"spring\", stiffness: 500, damping: 30 }}\n                  width={13}\n                  height={13}\n                  viewBox=\"0 0 16 16\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  strokeWidth={2}\n                  strokeLinecap=\"round\"\n                  strokeLinejoin=\"round\"\n                >\n                  <path d=\"M3 8.5l3.5 3.5L13 5\" />\n                </motion.svg>\n              ) : (\n                <motion.svg\n                  key=\"copy\"\n                  initial={{ opacity: 0, scale: 0.5 }}\n                  animate={{ opacity: 1, scale: 1 }}\n                  exit={{ opacity: 0, scale: 0.5 }}\n                  transition={{ type: \"spring\", stiffness: 500, damping: 30 }}\n                  width={13}\n                  height={13}\n                  viewBox=\"0 0 16 16\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  strokeWidth={1.5}\n                >\n                  <rect x=\"5\" y=\"5\" width=\"8\" height=\"8\" rx=\"1.5\" />\n                  <path d=\"M3 11V3.5A1.5 1.5 0 014.5 2H11\" />\n                </motion.svg>\n              )}\n            </AnimatePresence>\n          </motion.button>\n        </div>\n      </motion.div>\n    </>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ruixen/color-picker-input.tsx"
    }
  ]
}