{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "aurora-color-picker",
  "title": "Aurora ColorPicker",
  "author": "jmagar <https://github.com/jmagar>",
  "description": "Aurora ColorPicker — Claude Design parity.",
  "dependencies": [
    "lucide-react@^0.487.0"
  ],
  "registryDependencies": [
    "@aurora/aurora-tokens",
    "@aurora/aurora-components"
  ],
  "files": [
    {
      "path": "registry/aurora/ui/color-picker.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\n\n// --- color math -------------------------------------------------------------\n\ninterface Hsv {\n  h: number // 0..360\n  s: number // 0..1\n  v: number // 0..1\n}\n\nfunction clamp(n: number, min: number, max: number): number {\n  return Math.min(max, Math.max(min, n))\n}\n\nfunction hexToRgb(hex: string): { r: number; g: number; b: number } | null {\n  const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim())\n  if (!m) return null\n  const int = parseInt(m[1], 16)\n  return { r: (int >> 16) & 255, g: (int >> 8) & 255, b: int & 255 }\n}\n\nfunction rgbToHex(r: number, g: number, b: number): string {\n  const to = (n: number) => clamp(Math.round(n), 0, 255).toString(16).padStart(2, \"0\")\n  return `#${to(r)}${to(g)}${to(b)}`\n}\n\nfunction rgbToHsv(r: number, g: number, b: number): Hsv {\n  const rr = r / 255\n  const gg = g / 255\n  const bb = b / 255\n  const max = Math.max(rr, gg, bb)\n  const min = Math.min(rr, gg, bb)\n  const d = max - min\n  let h = 0\n  if (d !== 0) {\n    if (max === rr) h = ((gg - bb) / d) % 6\n    else if (max === gg) h = (bb - rr) / d + 2\n    else h = (rr - gg) / d + 4\n    h *= 60\n    if (h < 0) h += 360\n  }\n  const s = max === 0 ? 0 : d / max\n  return { h, s, v: max }\n}\n\nfunction hsvToRgb({ h, s, v }: Hsv): { r: number; g: number; b: number } {\n  const c = v * s\n  const x = c * (1 - Math.abs(((h / 60) % 2) - 1))\n  const m = v - c\n  let r = 0\n  let g = 0\n  let b = 0\n  if (h < 60) [r, g, b] = [c, x, 0]\n  else if (h < 120) [r, g, b] = [x, c, 0]\n  else if (h < 180) [r, g, b] = [0, c, x]\n  else if (h < 240) [r, g, b] = [0, x, c]\n  else if (h < 300) [r, g, b] = [x, 0, c]\n  else [r, g, b] = [c, 0, x]\n  return { r: (r + m) * 255, g: (g + m) * 255, b: (b + m) * 255 }\n}\n\nfunction hexToHsv(hex: string): Hsv | null {\n  const rgb = hexToRgb(hex)\n  if (!rgb) return null\n  return rgbToHsv(rgb.r, rgb.g, rgb.b)\n}\n\nfunction hsvToHex(hsv: Hsv): string {\n  const { r, g, b } = hsvToRgb(hsv)\n  return rgbToHex(r, g, b)\n}\n\n// Pure-hue hex (full saturation + value) for the saturation/value plane bg.\nfunction hueHex(h: number): string {\n  return hsvToHex({ h, s: 1, v: 1 })\n}\n\n// Styles: registry/aurora/styles/aurora-components.css (@layer aurora-components).\n\n// --- component --------------------------------------------------------------\n\nexport interface ColorPickerProps\n  extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onChange\" | \"defaultValue\"> {\n  /** Caption rendered above the picker (uppercased). */\n  label?: string\n  /** Controlled hex value (e.g. `#29b6f6`). */\n  value?: string\n  /** Uncontrolled initial hex value. */\n  defaultValue?: string\n  /** Quick-pick swatch colors rendered below the input. */\n  colors?: string[]\n  /** Fires with the new hex string whenever the color changes. */\n  onValueChange?: (hex: string) => void\n}\n\nconst DEFAULT_COLOR = \"#29b6f6\"\n\nfunction ColorPicker(\n  {\n    className,\n    label,\n    value,\n    defaultValue = DEFAULT_COLOR,\n    colors,\n    onValueChange,\n    style,\n    ref,\n    ...props\n  }: ColorPickerProps & { ref?: React.Ref<HTMLDivElement> }\n) {\n    const controlled = value !== undefined\n\n    // Internal source of truth is HSV so dragging on the SV plane at v=0/s=0\n    // doesn't lose the hue. Hex is derived for display / emit.\n    const [hsv, setHsv] = React.useState<Hsv>(\n      () => hexToHsv((controlled ? value : defaultValue) ?? DEFAULT_COLOR) ?? { h: 199, s: 0.83, v: 0.96 }\n    )\n    const [draft, setDraft] = React.useState<string>(\n      () => (controlled ? value : defaultValue) ?? DEFAULT_COLOR\n    )\n\n    // Sync from a controlled value prop.\n    React.useEffect(() => {\n      if (!controlled) return\n      const next = hexToHsv(value ?? DEFAULT_COLOR)\n      if (next) {\n        // eslint-disable-next-line react-hooks/set-state-in-effect\n        setHsv(next)\n        setDraft(value ?? DEFAULT_COLOR)\n      }\n    }, [controlled, value])\n\n    const hex = hsvToHex(hsv)\n\n    const commit = React.useCallback(\n      (next: Hsv) => {\n        if (!controlled) {\n          setHsv(next)\n          setDraft(hsvToHex(next))\n        }\n        onValueChange?.(hsvToHex(next))\n      },\n      [controlled, onValueChange]\n    )\n\n    const svRef = React.useRef<HTMLDivElement>(null)\n    const hueRef = React.useRef<HTMLDivElement>(null)\n\n    function pointerToSv(clientX: number, clientY: number) {\n      const el = svRef.current\n      if (!el) return\n      const rect = el.getBoundingClientRect()\n      const s = clamp((clientX - rect.left) / rect.width, 0, 1)\n      const v = clamp(1 - (clientY - rect.top) / rect.height, 0, 1)\n      commit({ ...hsv, s, v })\n    }\n\n    function pointerToHue(clientX: number) {\n      const el = hueRef.current\n      if (!el) return\n      const rect = el.getBoundingClientRect()\n      const h = clamp((clientX - rect.left) / rect.width, 0, 1) * 360\n      commit({ ...hsv, h })\n    }\n\n    function makeDragHandler(move: (x: number, y: number) => void) {\n      return (e: React.PointerEvent) => {\n        e.preventDefault()\n        const target = e.currentTarget as HTMLElement\n        target.setPointerCapture?.(e.pointerId)\n        move(e.clientX, e.clientY)\n        const onMove = (ev: PointerEvent) => move(ev.clientX, ev.clientY)\n        const onUp = () => {\n          window.removeEventListener(\"pointermove\", onMove)\n          window.removeEventListener(\"pointerup\", onUp)\n        }\n        window.addEventListener(\"pointermove\", onMove)\n        window.addEventListener(\"pointerup\", onUp)\n      }\n    }\n\n    const onSvKeyDown = (e: React.KeyboardEvent) => {\n      const step = e.shiftKey ? 0.1 : 0.02\n      let { s, v } = hsv\n      switch (e.key) {\n        case \"ArrowLeft\": s = clamp(s - step, 0, 1); break\n        case \"ArrowRight\": s = clamp(s + step, 0, 1); break\n        case \"ArrowUp\": v = clamp(v + step, 0, 1); break\n        case \"ArrowDown\": v = clamp(v - step, 0, 1); break\n        default: return\n      }\n      e.preventDefault()\n      commit({ ...hsv, s, v })\n    }\n\n    const onHueKeyDown = (e: React.KeyboardEvent) => {\n      const step = e.shiftKey ? 10 : 2\n      let h = hsv.h\n      if (e.key === \"ArrowLeft\" || e.key === \"ArrowDown\") h = (h - step + 360) % 360\n      else if (e.key === \"ArrowRight\" || e.key === \"ArrowUp\") h = (h + step) % 360\n      else return\n      e.preventDefault()\n      commit({ ...hsv, h })\n    }\n\n    function onHexChange(e: React.ChangeEvent<HTMLInputElement>) {\n      const raw = e.target.value.replace(/[^0-9a-fA-F]/g, \"\").slice(0, 6)\n      setDraft(`#${raw}`)\n      const parsed = hexToHsv(`#${raw}`)\n      if (parsed) commit(parsed)\n    }\n\n    function onHexBlur() {\n      setDraft(hex)\n    }\n\n    const draftValue = draft.replace(/^#/, \"\").toUpperCase()\n\n    return (\n      <div\n        ref={ref}\n        className={cn(\"aurora-cp\", className)}\n        style={{ [\"--aurora-cp-color\" as string]: hex, ...style }}\n        {...props}\n      >\n        {label ? <p className=\"aurora-cp__label\">{label}</p> : null}\n\n        {/* Saturation / value plane */}\n        <div\n          ref={svRef}\n          className=\"aurora-cp__sv\"\n          role=\"slider\"\n          tabIndex={0}\n          aria-label={`${label ? `${label} ` : \"\"}saturation and brightness`}\n          aria-valuemin={0}\n          aria-valuemax={100}\n          aria-valuenow={Math.round(hsv.v * 100)}\n          aria-valuetext={`saturation ${Math.round(hsv.s * 100)}%, brightness ${Math.round(hsv.v * 100)}%`}\n          // eslint-disable-next-line react-hooks/refs\n          onPointerDown={makeDragHandler(pointerToSv)}\n          onKeyDown={onSvKeyDown}\n          style={{\n            background: `linear-gradient(to top, #000 0%, transparent 100%), linear-gradient(to right, #fff 0%, ${hueHex(hsv.h)} 100%)`,\n          }}\n        >\n          <span\n            className=\"aurora-cp__sv-handle\"\n            style={{\n              left: `${hsv.s * 100}%`,\n              top: `${(1 - hsv.v) * 100}%`,\n              background: hex,\n            }}\n          />\n        </div>\n\n        {/* Hue slider */}\n        <div\n          ref={hueRef}\n          className=\"aurora-cp__hue\"\n          role=\"slider\"\n          tabIndex={0}\n          aria-label={`${label ? `${label} ` : \"\"}hue`}\n          aria-valuemin={0}\n          aria-valuemax={360}\n          aria-valuenow={Math.round(hsv.h)}\n          // eslint-disable-next-line react-hooks/refs\n          onPointerDown={makeDragHandler((x) => pointerToHue(x))}\n          onKeyDown={onHueKeyDown}\n        >\n          <span\n            className=\"aurora-cp__hue-handle\"\n            style={{ left: `${(hsv.h / 360) * 100}%`, background: hueHex(hsv.h) }}\n          />\n        </div>\n\n        {/* Preview + hex input */}\n        <div className=\"aurora-cp__row\">\n          <div className=\"aurora-cp__preview\" aria-hidden=\"true\" />\n          <div className=\"aurora-cp__input-wrap\">\n            <span className=\"aurora-cp__hash\" aria-hidden=\"true\">#</span>\n            <input\n              className=\"aurora-cp__input\"\n              value={draftValue}\n              onChange={onHexChange}\n              onBlur={onHexBlur}\n              spellCheck={false}\n              autoComplete=\"off\"\n              aria-label={`${label ? `${label} ` : \"\"}hex value`}\n            />\n          </div>\n        </div>\n\n        {/* Quick-pick swatches */}\n        {colors && colors.length > 0 ? (\n          <div className=\"aurora-cp__swatches\" role=\"group\" aria-label=\"Preset colors\">\n            {colors.map((c) => {\n              const selected = c.toLowerCase() === hex.toLowerCase()\n              return (\n                <button\n                  key={c}\n                  type=\"button\"\n                  className=\"aurora-cp__swatch\"\n                  data-selected={selected}\n                  aria-label={c}\n                  aria-pressed={selected}\n                  style={{ [\"--aurora-cp-swatch\" as string]: c }}\n                  onClick={() => {\n                    const next = hexToHsv(c)\n                    if (next) commit(next)\n                  }}\n                />\n              )\n            })}\n          </div>\n        ) : null}\n      </div>\n    )\n}\n\nexport { ColorPicker }\nexport default ColorPicker\n",
      "type": "registry:ui",
      "target": "@ui/aurora/color-picker.tsx"
    }
  ],
  "meta": {
    "namespace": "@aurora",
    "style": "aurora",
    "family": "ui",
    "requiresTokens": true,
    "sourcePath": "registry/aurora/ui/color-picker.tsx",
    "installTarget": "@ui/aurora/color-picker.tsx"
  },
  "docs": "Aurora ColorPicker is an Aurora UI component for Claude Design parity surfaces.\n\nInstall `aurora-tokens` and `aurora-components` first; the shadcn CLI resolves these automatically through the @aurora registry namespace.\n\nDefault install target: `@ui/aurora/color-picker.tsx`.\n\nRegistry dependencies: `aurora-tokens`, `aurora-components`.",
  "categories": [
    "aurora",
    "ui",
    "extension"
  ],
  "type": "registry:ui"
}