{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"color-picker","type":"registry:ui","title":"color-picker","description":"Allows users to select a color. Modeled after the color picker in Figma.","author":"Hayden Bleasel <hello@haydenbleasel.com>","dependencies":["color","lucide-react","radix-ui"],"devDependencies":["@types/color"],"registryDependencies":["button","input","select"],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport Color from \"color\";\nimport { PipetteIcon } from \"lucide-react\";\nimport { Slider } from \"radix-ui\";\nimport {\n  type ComponentProps,\n  createContext,\n  type HTMLAttributes,\n  memo,\n  useCallback,\n  useContext,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\";\nimport { cn } from \"@/lib/utils\";\n\ntype ColorPickerContextValue = {\n  hue: number;\n  saturation: number;\n  lightness: number;\n  alpha: number;\n  mode: string;\n  setHue: (hue: number) => void;\n  setSaturation: (saturation: number) => void;\n  setLightness: (lightness: number) => void;\n  setAlpha: (alpha: number) => void;\n  setMode: (mode: string) => void;\n};\n\nconst ColorPickerContext = createContext<ColorPickerContextValue | undefined>(\n  undefined\n);\n\nexport const useColorPicker = () => {\n  const context = useContext(ColorPickerContext);\n\n  if (!context) {\n    throw new Error(\"useColorPicker must be used within a ColorPickerProvider\");\n  }\n\n  return context;\n};\n\nexport type ColorPickerProps = HTMLAttributes<HTMLDivElement> & {\n  value?: Parameters<typeof Color>[0];\n  defaultValue?: Parameters<typeof Color>[0];\n  onChange?: (value: Parameters<typeof Color.rgb>[0]) => void;\n};\n\nexport const ColorPicker = ({\n  value,\n  defaultValue = \"#000000\",\n  onChange,\n  className,\n  ...props\n}: ColorPickerProps) => {\n  const selectedColor = Color(value);\n  const defaultColor = Color(defaultValue);\n\n  const [hue, setHue] = useState(\n    selectedColor.hue() || defaultColor.hue() || 0\n  );\n  const [saturation, setSaturation] = useState(\n    selectedColor.saturationl() || defaultColor.saturationl() || 100\n  );\n  const [lightness, setLightness] = useState(\n    selectedColor.lightness() || defaultColor.lightness() || 50\n  );\n  const [alpha, setAlpha] = useState(\n    selectedColor.alpha() * 100 || defaultColor.alpha() * 100\n  );\n  const [mode, setMode] = useState(\"hex\");\n\n  // Update color when controlled value changes\n  useEffect(() => {\n    if (value) {\n      const color = Color.rgb(value).rgb().object();\n\n      setHue(color.r);\n      setSaturation(color.g);\n      setLightness(color.b);\n      setAlpha(color.a);\n    }\n  }, [value]);\n\n  // Notify parent of changes\n  useEffect(() => {\n    if (onChange) {\n      const color = Color.hsl(hue, saturation, lightness).alpha(alpha / 100);\n      const rgba = color.rgb().array();\n\n      onChange([rgba[0], rgba[1], rgba[2], alpha / 100]);\n    }\n  }, [hue, saturation, lightness, alpha, onChange]);\n\n  return (\n    <ColorPickerContext.Provider\n      value={{\n        hue,\n        saturation,\n        lightness,\n        alpha,\n        mode,\n        setHue,\n        setSaturation,\n        setLightness,\n        setAlpha,\n        setMode,\n      }}\n    >\n      <div\n        className={cn(\"flex size-full flex-col gap-4\", className)}\n        {...props}\n      />\n    </ColorPickerContext.Provider>\n  );\n};\n\nexport type ColorPickerSelectionProps = HTMLAttributes<HTMLDivElement>;\n\nexport const ColorPickerSelection = memo(\n  ({ className, ...props }: ColorPickerSelectionProps) => {\n    const containerRef = useRef<HTMLDivElement>(null);\n    const [isDragging, setIsDragging] = useState(false);\n    const [positionX, setPositionX] = useState(0);\n    const [positionY, setPositionY] = useState(0);\n    const { hue, setSaturation, setLightness } = useColorPicker();\n\n    const backgroundGradient = useMemo(() => {\n      return `linear-gradient(0deg, rgba(0,0,0,1), rgba(0,0,0,0)),\n            linear-gradient(90deg, rgba(255,255,255,1), rgba(255,255,255,0)),\n            hsl(${hue}, 100%, 50%)`;\n    }, [hue]);\n\n    const handlePointerMove = useCallback(\n      (event: PointerEvent) => {\n        if (!(isDragging && containerRef.current)) {\n          return;\n        }\n        const rect = containerRef.current.getBoundingClientRect();\n        const x = Math.max(\n          0,\n          Math.min(1, (event.clientX - rect.left) / rect.width)\n        );\n        const y = Math.max(\n          0,\n          Math.min(1, (event.clientY - rect.top) / rect.height)\n        );\n        setPositionX(x);\n        setPositionY(y);\n        setSaturation(x * 100);\n        const topLightness = x < 0.01 ? 100 : 50 + 50 * (1 - x);\n        const lightness = topLightness * (1 - y);\n\n        setLightness(lightness);\n      },\n      [isDragging, setSaturation, setLightness]\n    );\n\n    useEffect(() => {\n      const handlePointerUp = () => setIsDragging(false);\n\n      if (isDragging) {\n        window.addEventListener(\"pointermove\", handlePointerMove);\n        window.addEventListener(\"pointerup\", handlePointerUp);\n      }\n\n      return () => {\n        window.removeEventListener(\"pointermove\", handlePointerMove);\n        window.removeEventListener(\"pointerup\", handlePointerUp);\n      };\n    }, [isDragging, handlePointerMove]);\n\n    return (\n      <div\n        className={cn(\"relative size-full cursor-crosshair rounded\", className)}\n        onPointerDown={(e) => {\n          e.preventDefault();\n          setIsDragging(true);\n          handlePointerMove(e.nativeEvent);\n        }}\n        ref={containerRef}\n        style={{\n          background: backgroundGradient,\n        }}\n        {...props}\n      >\n        <div\n          className=\"-translate-x-1/2 -translate-y-1/2 pointer-events-none absolute h-4 w-4 rounded-full border-2 border-white\"\n          style={{\n            left: `${positionX * 100}%`,\n            top: `${positionY * 100}%`,\n            boxShadow: \"0 0 0 1px rgba(0,0,0,0.5)\",\n          }}\n        />\n      </div>\n    );\n  }\n);\n\nColorPickerSelection.displayName = \"ColorPickerSelection\";\n\nexport type ColorPickerHueProps = ComponentProps<typeof Slider.Root>;\n\nexport const ColorPickerHue = ({\n  className,\n  ...props\n}: ColorPickerHueProps) => {\n  const { hue, setHue } = useColorPicker();\n\n  return (\n    <Slider.Root\n      className={cn(\"relative flex h-4 w-full touch-none\", className)}\n      max={360}\n      onValueChange={([hue]) => setHue(hue)}\n      step={1}\n      value={[hue]}\n      {...props}\n    >\n      <Slider.Track className=\"relative my-0.5 h-3 w-full grow rounded-full bg-[linear-gradient(90deg,#FF0000,#FFFF00,#00FF00,#00FFFF,#0000FF,#FF00FF,#FF0000)]\">\n        <Slider.Range className=\"absolute h-full\" />\n      </Slider.Track>\n      <Slider.Thumb className=\"block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50\" />\n    </Slider.Root>\n  );\n};\n\nexport type ColorPickerAlphaProps = ComponentProps<typeof Slider.Root>;\n\nexport const ColorPickerAlpha = ({\n  className,\n  ...props\n}: ColorPickerAlphaProps) => {\n  const { alpha, setAlpha } = useColorPicker();\n\n  return (\n    <Slider.Root\n      className={cn(\"relative flex h-4 w-full touch-none\", className)}\n      max={100}\n      onValueChange={([alpha]) => setAlpha(alpha)}\n      step={1}\n      value={[alpha]}\n      {...props}\n    >\n      <Slider.Track className=\"relative my-0.5 h-3 w-full grow rounded-full bg-[url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==')] bg-center bg-repeat-x dark:bg-[url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAALklEQVR4nGP8+vWrCAMewM3N/QafPBM+SWLAqAGDwQBGQgoIpZOB98KoAVQwAADxzQcSVIRCfQAAAABJRU5ErkJggg==')]\">\n        <div className=\"absolute inset-0 rounded-full bg-gradient-to-r from-transparent to-black/50 dark:to-white/50\" />\n        <Slider.Range className=\"absolute h-full rounded-full bg-transparent\" />\n      </Slider.Track>\n      <Slider.Thumb className=\"block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50\" />\n    </Slider.Root>\n  );\n};\n\nexport type ColorPickerEyeDropperProps = ComponentProps<typeof Button>;\n\nexport const ColorPickerEyeDropper = ({\n  className,\n  ...props\n}: ColorPickerEyeDropperProps) => {\n  const { setHue, setSaturation, setLightness, setAlpha } = useColorPicker();\n\n  const handleEyeDropper = async () => {\n    try {\n      // @ts-expect-error - EyeDropper API is experimental\n      const eyeDropper = new EyeDropper();\n      const result = await eyeDropper.open();\n      const color = Color(result.sRGBHex);\n      const [h, s, l] = color.hsl().array();\n\n      setHue(h);\n      setSaturation(s);\n      setLightness(l);\n      setAlpha(100);\n    } catch (error) {\n      console.error(\"EyeDropper failed:\", error);\n    }\n  };\n\n  return (\n    <Button\n      className={cn(\"shrink-0 text-muted-foreground\", className)}\n      onClick={handleEyeDropper}\n      size=\"icon\"\n      type=\"button\"\n      variant=\"outline\"\n      {...props}\n    >\n      <PipetteIcon size={16} />\n    </Button>\n  );\n};\n\nexport type ColorPickerOutputProps = ComponentProps<typeof SelectTrigger>;\n\nconst formats = [\"hex\", \"rgb\", \"css\", \"hsl\"];\n\nexport const ColorPickerOutput = ({\n  className,\n  ...props\n}: ColorPickerOutputProps) => {\n  const { mode, setMode } = useColorPicker();\n\n  return (\n    <Select onValueChange={setMode} value={mode}>\n      <SelectTrigger className=\"h-8 w-20 shrink-0 text-xs\" {...props}>\n        <SelectValue placeholder=\"Mode\" />\n      </SelectTrigger>\n      <SelectContent>\n        {formats.map((format) => (\n          <SelectItem className=\"text-xs\" key={format} value={format}>\n            {format.toUpperCase()}\n          </SelectItem>\n        ))}\n      </SelectContent>\n    </Select>\n  );\n};\n\ntype PercentageInputProps = ComponentProps<typeof Input>;\n\nconst PercentageInput = ({ className, ...props }: PercentageInputProps) => {\n  return (\n    <div className=\"relative\">\n      <Input\n        readOnly\n        type=\"text\"\n        {...props}\n        className={cn(\n          \"h-8 w-[3.25rem] rounded-l-none bg-secondary px-2 text-xs shadow-none\",\n          className\n        )}\n      />\n      <span className=\"-translate-y-1/2 absolute top-1/2 right-2 text-muted-foreground text-xs\">\n        %\n      </span>\n    </div>\n  );\n};\n\nexport type ColorPickerFormatProps = HTMLAttributes<HTMLDivElement>;\n\nexport const ColorPickerFormat = ({\n  className,\n  ...props\n}: ColorPickerFormatProps) => {\n  const { hue, saturation, lightness, alpha, mode } = useColorPicker();\n  const color = Color.hsl(hue, saturation, lightness, alpha / 100);\n\n  if (mode === \"hex\") {\n    const hex = color.hex();\n\n    return (\n      <div\n        className={cn(\n          \"-space-x-px relative flex w-full items-center rounded-md shadow-sm\",\n          className\n        )}\n        {...props}\n      >\n        <Input\n          className=\"h-8 rounded-r-none bg-secondary px-2 text-xs shadow-none\"\n          readOnly\n          type=\"text\"\n          value={hex}\n        />\n        <PercentageInput value={alpha} />\n      </div>\n    );\n  }\n\n  if (mode === \"rgb\") {\n    const rgb = color\n      .rgb()\n      .array()\n      .map((value) => Math.round(value));\n\n    return (\n      <div\n        className={cn(\n          \"-space-x-px flex items-center rounded-md shadow-sm\",\n          className\n        )}\n        {...props}\n      >\n        {rgb.map((value, index) => (\n          <Input\n            className={cn(\n              \"h-8 rounded-r-none bg-secondary px-2 text-xs shadow-none\",\n              index && \"rounded-l-none\",\n              className\n            )}\n            key={index}\n            readOnly\n            type=\"text\"\n            value={value}\n          />\n        ))}\n        <PercentageInput value={alpha} />\n      </div>\n    );\n  }\n\n  if (mode === \"css\") {\n    const rgb = color\n      .rgb()\n      .array()\n      .map((value) => Math.round(value));\n\n    return (\n      <div className={cn(\"w-full rounded-md shadow-sm\", className)} {...props}>\n        <Input\n          className=\"h-8 w-full bg-secondary px-2 text-xs shadow-none\"\n          readOnly\n          type=\"text\"\n          value={`rgba(${rgb.join(\", \")}, ${alpha}%)`}\n          {...props}\n        />\n      </div>\n    );\n  }\n\n  if (mode === \"hsl\") {\n    const hsl = color\n      .hsl()\n      .array()\n      .map((value) => Math.round(value));\n\n    return (\n      <div\n        className={cn(\n          \"-space-x-px flex items-center rounded-md shadow-sm\",\n          className\n        )}\n        {...props}\n      >\n        {hsl.map((value, index) => (\n          <Input\n            className={cn(\n              \"h-8 rounded-r-none bg-secondary px-2 text-xs shadow-none\",\n              index && \"rounded-l-none\",\n              className\n            )}\n            key={index}\n            readOnly\n            type=\"text\"\n            value={value}\n          />\n        ))}\n        <PercentageInput value={alpha} />\n      </div>\n    );\n  }\n\n  return null;\n};\n","target":"components/kibo-ui/color-picker/index.tsx"}],"css":{}}