{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "color-picker",
  "title": "Color Picker",
  "description": "Color picker component",
  "registryDependencies": [
    "button",
    "input"
  ],
  "files": [
    {
      "path": "components/ui/color-picker.tsx",
      "content": "'use client'\n\nimport { IconColorPicker } from '@tabler/icons-react'\nimport { cva } from 'class-variance-authority'\nimport { Slider as SliderPrimitive } from 'radix-ui'\nimport * as React from 'react'\n\nimport { cn } from '@/lib/cn'\n\nimport { Button } from './button'\nimport { Input } from './input'\n\ninterface Hsva {\n  h: number\n  s: number\n  v: number\n  a: number\n}\n\nfunction clamp(value: number, min: number, max: number) {\n  return Math.min(max, Math.max(min, value))\n}\n\nfunction hsvToRgb({ h, s, v }: Pick<Hsva, 'h' | 's' | 'v'>): [number, number, number] {\n  const f = (n: number) => {\n    const k = (n + h / 60) % 6\n    return (v / 100) * (1 - (s / 100) * Math.max(0, Math.min(k, 4 - k, 1)))\n  }\n  return [Math.round(f(5) * 255), Math.round(f(3) * 255), Math.round(f(1) * 255)]\n}\n\nfunction rgbToHsv([r, g, b]: [number, number, number]): Pick<Hsva, 'h' | 's' | 'v'> {\n  const rn = r / 255\n  const gn = g / 255\n  const bn = b / 255\n  const max = Math.max(rn, gn, bn)\n  const delta = max - Math.min(rn, gn, bn)\n  let h = 0\n  if (delta > 0) {\n    if (max === rn) h = 60 * (((gn - bn) / delta) % 6)\n    else if (max === gn) h = 60 * ((bn - rn) / delta + 2)\n    else h = 60 * ((rn - gn) / delta + 4)\n    if (h < 0) h += 360\n  }\n  return { h, s: max === 0 ? 0 : (delta / max) * 100, v: max * 100 }\n}\n\nfunction hexToRgba(hex: string): { rgb: [number, number, number]; a: number } | null {\n  const matched = /^#?([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(hex.trim())?.[1]\n  if (!matched) return null\n  const full = matched.length <= 4 ? matched.replace(/./g, char => char + char) : matched\n  const int = Number.parseInt(full.slice(0, 6), 16)\n  // Alpha stays a float (not rounded to a percent) so hex byte round-trips exactly\n  const alphaByte = full.length === 8 ? Number.parseInt(full.slice(6), 16) : 255\n  return { rgb: [(int >> 16) & 0xff, (int >> 8) & 0xff, int & 0xff], a: (alphaByte / 255) * 100 }\n}\n\nfunction rgbToHex([r, g, b]: [number, number, number]) {\n  return `#${[r, g, b].map(channel => channel.toString(16).padStart(2, '0').toUpperCase()).join('')}`\n}\n\n// 6-digit when fully opaque, 8-digit otherwise — alpha stays out of the value\n// entirely for compositions that never render an alpha slider\nfunction formatHex(rgb: [number, number, number], alpha: number) {\n  const alphaByte = Math.round((alpha / 100) * 255)\n  return alphaByte === 255 ? rgbToHex(rgb) : `${rgbToHex(rgb)}${alphaByte.toString(16).padStart(2, '0').toUpperCase()}`\n}\n\n// Layers a top background over a checkerboard so translucency reads honestly\nfunction overCheckerboard(topLayer: string): React.CSSProperties {\n  return {\n    backgroundColor: '#fff',\n    backgroundImage: `${topLayer}, conic-gradient(rgba(0,0,0,0.15) 0 25%, transparent 0 50%, rgba(0,0,0,0.15) 0 75%, transparent 0)`,\n    backgroundSize: '100% 100%, 0.5rem 0.5rem',\n  }\n}\n\ninterface ColorPickerContextValue {\n  /** Hue in degrees, 0–360 */\n  hue: number\n  /** Saturation percentage, 0–100 */\n  saturation: number\n  /** Brightness (HSV value) percentage, 0–100 */\n  brightness: number\n  /** Alpha percentage, 0–100 */\n  alpha: number\n  /** Current color as an uppercase hex string — #RRGGBB, or #RRGGBBAA when alpha < 100 */\n  hex: string\n  setHsv: (hsv: Partial<Hsva>) => void\n  setHex: (hex: string) => boolean\n}\n\nconst ColorPickerContext = React.createContext<ColorPickerContextValue | null>(null)\n\nfunction useColorPicker() {\n  const context = React.useContext(ColorPickerContext)\n  if (!context) throw new Error('useColorPicker must be used within <ColorPicker>')\n  return context\n}\n\nexport interface ColorPickerProps extends React.ComponentProps<'div'> {\n  /** Controlled color as a 3-, 4-, 6- or 8-digit hex string */\n  value?: string\n  /** Initial color for uncontrolled usage */\n  defaultValue?: string\n  onValueChange?: (value: string) => void\n}\n\nfunction ColorPicker({ value, defaultValue = '#000000', onValueChange, className, ...props }: ColorPickerProps) {\n  const [hsva, setHsvaState] = React.useState<Hsva>(() => {\n    const parsed = hexToRgba(value ?? defaultValue)\n    return parsed ? { ...rgbToHsv(parsed.rgb), a: parsed.a } : { h: 0, s: 0, v: 0, a: 100 }\n  })\n  const hex = formatHex(hsvToRgb(hsva), hsva.a)\n\n  // Re-derive HSV when a controlled `value` changes externally. Skipped when the\n  // incoming color already matches, so echoes of our own onValueChange don't\n  // collapse hue/saturation (all grays share s=0, all blacks share v=0).\n  const [prevValue, setPrevValue] = React.useState(value)\n  if (value !== prevValue) {\n    setPrevValue(value)\n    const parsed = value === undefined ? null : hexToRgba(value)\n    if (parsed && formatHex(parsed.rgb, parsed.a) !== hex) setHsvaState({ ...rgbToHsv(parsed.rgb), a: parsed.a })\n  }\n\n  const update = (next: Hsva) => {\n    setHsvaState(next)\n    const nextHex = formatHex(hsvToRgb(next), next.a)\n    if (nextHex !== hex) onValueChange?.(nextHex)\n  }\n\n  const setHsv = (partial: Partial<Hsva>) =>\n    update({\n      h: clamp(partial.h ?? hsva.h, 0, 360),\n      s: clamp(partial.s ?? hsva.s, 0, 100),\n      v: clamp(partial.v ?? hsva.v, 0, 100),\n      a: clamp(partial.a ?? hsva.a, 0, 100),\n    })\n\n  const setHex = (nextHex: string) => {\n    const parsed = hexToRgba(nextHex)\n    if (!parsed) return false\n    const next = rgbToHsv(parsed.rgb)\n    // A hex round-trip cannot encode hue for grays (nor saturation for black);\n    // keep the current ones so the area/slider don't jump when typing #FFF/#000.\n    update({\n      h: next.s === 0 || next.v === 0 ? hsva.h : next.h,\n      s: next.v === 0 ? hsva.s : next.s,\n      v: next.v,\n      a: parsed.a,\n    })\n    return true\n  }\n\n  return (\n    <ColorPickerContext.Provider\n      value={{ hue: hsva.h, saturation: hsva.s, brightness: hsva.v, alpha: hsva.a, hex, setHsv, setHex }}\n    >\n      <div data-slot='color-picker' className={cn('flex w-full flex-col gap-3', className)} {...props} />\n    </ColorPickerContext.Provider>\n  )\n}\n\n// Per-part classes, mirroring the `cva` record pattern used by Slider.\nconst colorPickerVariants = {\n  // Shared by the area thumb and both slider thumbs. The white border is\n  // deliberate: thumbs ride on color surfaces that render identically in both\n  // themes, while `ring-fg/20` adapts to the page background behind them.\n  // Focus is an exception to `focus-ring`: thumbs are focused while dragging,\n  // and an accent border/ring is illegible over saturated surfaces — stay white.\n  thumb: cva(\n    'block size-5 rounded-full border-2 border-white shadow-sm ring-1 ring-fg/20 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/30'\n  ),\n}\n\nfunction ColorPickerArea({ className, ...props }: React.ComponentProps<'div'>) {\n  const { hue, saturation, brightness, setHsv } = useColorPicker()\n  const thumbRef = React.useRef<HTMLDivElement>(null)\n\n  const moveTo = (event: React.PointerEvent<HTMLDivElement>) => {\n    const rect = event.currentTarget.getBoundingClientRect()\n    setHsv({\n      s: ((event.clientX - rect.left) / rect.width) * 100,\n      v: (1 - (event.clientY - rect.top) / rect.height) * 100,\n    })\n  }\n\n  return (\n    <div\n      data-slot='color-picker-area'\n      className={cn('relative aspect-square w-full cursor-crosshair touch-none select-none rounded-lg', className)}\n      style={{\n        backgroundColor: `hsl(${hue} 100% 50%)`,\n        backgroundImage: 'linear-gradient(to top, #000, transparent), linear-gradient(to right, #fff, transparent)',\n      }}\n      onPointerDown={event => {\n        event.preventDefault()\n        event.currentTarget.setPointerCapture(event.pointerId)\n        thumbRef.current?.focus()\n        moveTo(event)\n      }}\n      onPointerMove={event => {\n        if (event.currentTarget.hasPointerCapture(event.pointerId)) moveTo(event)\n      }}\n      {...props}\n    >\n      <div\n        ref={thumbRef}\n        data-slot='color-picker-area-thumb'\n        role='slider'\n        tabIndex={0}\n        aria-label='Color'\n        aria-valuemin={0}\n        aria-valuemax={100}\n        aria-valuenow={Math.round(saturation)}\n        aria-valuetext={`Saturation ${Math.round(saturation)}%, Brightness ${Math.round(brightness)}%`}\n        className={cn('absolute -translate-x-1/2 -translate-y-1/2', colorPickerVariants.thumb())}\n        style={{\n          left: `${saturation}%`,\n          top: `${100 - brightness}%`,\n          backgroundColor: rgbToHex(hsvToRgb({ h: hue, s: saturation, v: brightness })),\n        }}\n        onKeyDown={event => {\n          const step = event.shiftKey ? 10 : 1\n          const moves: Record<string, Partial<Hsva>> = {\n            ArrowLeft: { s: saturation - step },\n            ArrowRight: { s: saturation + step },\n            ArrowUp: { v: brightness + step },\n            ArrowDown: { v: brightness - step },\n          }\n          const move = moves[event.key]\n          if (!move) return\n          event.preventDefault()\n          setHsv(move)\n        }}\n      />\n    </div>\n  )\n}\n\ntype ColorPickerSliderProps = Omit<\n  React.ComponentProps<typeof SliderPrimitive.Root>,\n  'min' | 'max' | 'value' | 'defaultValue' | 'onValueChange'\n>\n\nfunction ColorPickerSlider({ className, ...props }: ColorPickerSliderProps) {\n  const { hue, setHsv } = useColorPicker()\n  return (\n    <SliderPrimitive.Root\n      data-slot='color-picker-slider'\n      step={1}\n      className={cn('relative flex w-full touch-none select-none items-center', className)}\n      {...props}\n      min={0}\n      max={360}\n      value={[hue]}\n      onValueChange={values => setHsv({ h: values[0] ?? hue })}\n    >\n      <SliderPrimitive.Track className='relative h-4 w-full grow rounded-full bg-[linear-gradient(to_right,#f00,#ff0,#0f0,#0ff,#00f,#f0f,#f00)]' />\n      <SliderPrimitive.Thumb\n        aria-label='Hue'\n        className={colorPickerVariants.thumb()}\n        style={{ backgroundColor: `hsl(${hue} 100% 50%)` }}\n      />\n    </SliderPrimitive.Root>\n  )\n}\n\nfunction ColorPickerAlphaSlider({ className, ...props }: ColorPickerSliderProps) {\n  const { hue, saturation, brightness, alpha, hex, setHsv } = useColorPicker()\n  const opaque = rgbToHex(hsvToRgb({ h: hue, s: saturation, v: brightness }))\n  return (\n    <SliderPrimitive.Root\n      data-slot='color-picker-alpha-slider'\n      step={1}\n      className={cn('relative flex w-full touch-none select-none items-center', className)}\n      {...props}\n      min={0}\n      max={100}\n      value={[alpha]}\n      onValueChange={values => setHsv({ a: values[0] ?? alpha })}\n    >\n      <SliderPrimitive.Track\n        className='relative h-4 w-full grow rounded-full'\n        style={overCheckerboard(`linear-gradient(to right, transparent, ${opaque})`)}\n      />\n      <SliderPrimitive.Thumb\n        aria-label='Alpha'\n        className={colorPickerVariants.thumb()}\n        style={overCheckerboard(`linear-gradient(${hex}, ${hex})`)}\n      />\n    </SliderPrimitive.Root>\n  )\n}\n\ninterface EyeDropperInstance {\n  open: (options?: { signal?: AbortSignal }) => Promise<{ sRGBHex: string }>\n}\n\ndeclare global {\n  interface Window {\n    /** Chromium-only, missing from lib.dom. */\n    EyeDropper?: new () => EyeDropperInstance\n  }\n}\n\nfunction ColorPickerEyeDropper({ onClick, children, ...props }: React.ComponentProps<typeof Button>) {\n  const { setHsv } = useColorPicker()\n  const EyeDropper = window.EyeDropper\n\n  return (\n    <Button\n      data-slot='color-picker-eye-dropper'\n      variant='outline'\n      size='icon'\n      aria-label='Pick color from screen'\n      disabled={!EyeDropper}\n      {...props}\n      onClick={async event => {\n        onClick?.(event)\n        if (event.defaultPrevented) return\n        if (!EyeDropper) return\n        try {\n          const { sRGBHex } = await new EyeDropper().open()\n          const parsed = hexToRgba(sRGBHex)\n          if (!parsed) return\n          const next = rgbToHsv(parsed.rgb)\n          // Keep the current alpha, and the current hue when picking a gray\n          setHsv({ h: next.s === 0 || next.v === 0 ? undefined : next.h, s: next.s, v: next.v })\n        } catch {\n          // canceled by the user\n        }\n      }}\n    >\n      {children ?? <IconColorPicker />}\n    </Button>\n  )\n}\n\nfunction ColorPickerInput({\n  className,\n  onChange,\n  onBlur,\n  onKeyDown,\n  ...props\n}: Omit<React.ComponentProps<typeof Input>, 'value' | 'defaultValue'>) {\n  const { hex, setHex } = useColorPicker()\n  // While editing, the draft shadows the canonical hex so external updates\n  // (area/slider drags) don't rewrite the field mid-keystroke.\n  const [draft, setDraft] = React.useState<string | null>(null)\n\n  const commit = (raw: string) => {\n    setHex(raw)\n    setDraft(null)\n  }\n\n  return (\n    <Input\n      data-slot='color-picker-input'\n      aria-label='Hex color'\n      autoCapitalize='none'\n      autoComplete='off'\n      spellCheck={false}\n      className={cn('w-fit', className)}\n      {...props}\n      value={draft ?? hex}\n      onChange={event => {\n        const raw = event.target.value\n        setDraft(raw)\n        // Live-preview complete 6- or 8-digit values; shorter drafts commit on blur/Enter\n        if (/^#?[0-9a-f]{6}(?:[0-9a-f]{2})?$/i.test(raw)) setHex(raw)\n        onChange?.(event)\n      }}\n      onBlur={event => {\n        if (draft !== null) commit(draft)\n        onBlur?.(event)\n      }}\n      onKeyDown={event => {\n        if (event.key === 'Enter' && draft !== null) {\n          event.preventDefault()\n          commit(draft)\n        } else if (event.key === 'Escape' && draft !== null) {\n          event.stopPropagation()\n          setDraft(null)\n        }\n        onKeyDown?.(event)\n      }}\n    />\n  )\n}\n\nfunction ColorPickerSwatch({\n  className,\n  color,\n  style,\n  ...props\n}: React.ComponentProps<'div'> & {\n  /** Color to display; defaults to the current color of the surrounding ColorPicker */\n  color?: string\n}) {\n  const context = React.useContext(ColorPickerContext)\n  const swatchColor = color ?? context?.hex\n  return (\n    <div\n      data-slot='color-picker-swatch'\n      className={cn('size-5 shrink-0 rounded-md border border-fg/15 shadow-xs', className)}\n      style={{ ...(swatchColor && overCheckerboard(`linear-gradient(${swatchColor}, ${swatchColor})`)), ...style }}\n      {...props}\n    />\n  )\n}\n\nexport {\n  ColorPicker,\n  ColorPickerAlphaSlider,\n  ColorPickerArea,\n  ColorPickerEyeDropper,\n  ColorPickerInput,\n  ColorPickerSlider,\n  ColorPickerSwatch,\n  colorPickerVariants,\n  useColorPicker,\n}\n",
      "type": "registry:component"
    },
    {
      "path": "lib/cn.ts",
      "content": "import { type ClassValue, clsx } from 'clsx'\nimport { twMerge } from 'tailwind-merge'\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n",
      "type": "registry:lib"
    }
  ],
  "type": "registry:component"
}