{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "color-picker",
  "title": "Color Picker",
  "description": "Saturation area, hue and alpha sliders, eyedropper, hex input and an oklch readout.",
  "dependencies": [
    "radix-ui",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "button",
    "input",
    "popover",
    "@drift-ui/use-controllable-state"
  ],
  "files": [
    {
      "path": "registry/default/ui/color-picker.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { PipetteIcon } from \"lucide-react\"\n\nimport { cn } from \"@/registry/default/lib/utils\"\nimport {\n  formatHex,\n  formatOklch,\n  parseHex,\n  type Hsva,\n} from \"@/registry/default/lib/color\"\nimport { useControllableState } from \"@/registry/default/hooks/use-controllable-state\"\nimport { Button, buttonVariants } from \"@/registry/default/ui/button\"\nimport {\n  AlphaSlider,\n  ColorArea,\n  ColorSwatch,\n  HueSlider,\n} from \"@/registry/default/ui/color-picker-controls\"\nimport { Input } from \"@/registry/default/ui/input\"\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/registry/default/ui/popover\"\n\nconst BLACK: Hsva = { h: 0, s: 0, v: 0, a: 1 }\n\ntype EyeDropperConstructor = new () => {\n  open: () => Promise<{ sRGBHex: string }>\n}\n\nfunction subscribeToNothing() {\n  return () => {}\n}\n\nfunction readEyeDropper() {\n  return (window as { EyeDropper?: EyeDropperConstructor }).EyeDropper ?? null\n}\n\nfunction useEyeDropper() {\n  return React.useSyncExternalStore(\n    subscribeToNothing,\n    readEyeDropper,\n    () => null\n  )\n}\n\nfunction HexInput({\n  hex,\n  onCommit,\n}: {\n  hex: string\n  onCommit: (hex: string) => void\n}) {\n  const [draft, setDraft] = React.useState(hex)\n\n  return (\n    <Input\n      aria-label=\"Hex color\"\n      spellCheck={false}\n      value={draft}\n      aria-invalid={parseHex(draft) ? undefined : true}\n      onChange={(event) => setDraft(event.target.value)}\n      onBlur={() => onCommit(draft)}\n      onKeyDown={(event) => {\n        if (event.key === \"Enter\") onCommit(draft)\n      }}\n      className=\"h-8 font-mono text-xs\"\n    />\n  )\n}\n\nfunction ColorPicker({\n  value,\n  defaultValue = \"#000000\",\n  onValueChange,\n  showAlpha = true,\n  swatches,\n  variant = \"outline\",\n  size,\n  className,\n  ...props\n}: Omit<React.ComponentProps<\"button\">, \"value\" | \"defaultValue\" | \"onChange\"> &\n  Pick<React.ComponentProps<typeof Button>, \"variant\" | \"size\"> & {\n    value?: string\n    defaultValue?: string\n    onValueChange?: (hex: string) => void\n    showAlpha?: boolean\n    swatches?: string[]\n  }) {\n  const EyeDropper = useEyeDropper()\n  const { value: hex, setValue: setHex } = useControllableState({\n    value,\n    defaultValue,\n    onChange: onValueChange,\n  })\n  const [hsva, setHsva] = React.useState(() => parseHex(hex) ?? BLACK)\n  const [syncedHex, setSyncedHex] = React.useState(hex)\n\n  if (hex !== syncedHex) {\n    setSyncedHex(hex)\n    if (hex !== formatHex(hsva)) setHsva(parseHex(hex) ?? hsva)\n  }\n\n  function updateHsva(next: Hsva) {\n    const normalized = showAlpha ? next : { ...next, a: 1 }\n    const nextHex = formatHex(normalized)\n    setHsva(normalized)\n    setSyncedHex(nextHex)\n    setHex(nextHex)\n  }\n\n  function commitHex(candidate: string) {\n    const parsed = parseHex(candidate)\n    if (parsed) updateHsva(parsed)\n  }\n\n  async function pickFromScreen() {\n    if (!EyeDropper) return\n    try {\n      const { sRGBHex } = await new EyeDropper().open()\n      const picked = parseHex(sRGBHex)\n      if (picked) updateHsva({ ...picked, a: hsva.a })\n    } catch {\n      return\n    }\n  }\n\n  return (\n    <Popover>\n      <PopoverTrigger\n        data-slot=\"color-picker-trigger\"\n        className={cn(\n          buttonVariants({ variant, size }),\n          \"justify-start gap-2 font-mono text-xs\",\n          className\n        )}\n        {...props}\n      >\n        <ColorSwatch color={hex} />\n        {hex}\n      </PopoverTrigger>\n      <PopoverContent\n        data-slot=\"color-picker-content\"\n        align=\"start\"\n        className=\"flex w-64 flex-col gap-3 p-3\"\n      >\n        <ColorArea hsva={hsva} onHsvaChange={updateHsva} />\n        <div className=\"flex items-center gap-3\">\n          {EyeDropper && (\n            <Button\n              variant=\"ghost\"\n              size=\"icon\"\n              aria-label=\"Pick a color from the screen\"\n              onClick={pickFromScreen}\n              className=\"size-8\"\n            >\n              <PipetteIcon />\n            </Button>\n          )}\n          <div className=\"flex flex-1 flex-col gap-3\">\n            <HueSlider hsva={hsva} onHsvaChange={updateHsva} />\n            {showAlpha && <AlphaSlider hsva={hsva} onHsvaChange={updateHsva} />}\n          </div>\n        </div>\n        <HexInput key={hex} hex={hex} onCommit={commitHex} />\n        <output className=\"truncate font-mono text-[11px] text-muted-foreground select-all\">\n          {formatOklch(hsva)}\n        </output>\n        {swatches && (\n          <div className=\"grid grid-cols-8 gap-1.5\">\n            {swatches.map((swatch) => (\n              <button\n                key={swatch}\n                type=\"button\"\n                aria-label={`Use ${swatch}`}\n                onClick={() => commitHex(swatch)}\n                className=\"rounded-sm outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n              >\n                <ColorSwatch color={swatch} className=\"size-full\" />\n              </button>\n            ))}\n          </div>\n        )}\n      </PopoverContent>\n    </Popover>\n  )\n}\n\nexport { ColorPicker }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/default/ui/color-picker-controls.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Slider as SliderPrimitive } from \"radix-ui\"\n\nimport { cn } from \"@/registry/default/lib/utils\"\nimport { clampUnit, formatRgba, type Hsva } from \"@/registry/default/lib/color\"\n\nconst FINE_STEP = 0.01\nconst COARSE_STEP = 0.1\n\nconst CHECKERBOARD =\n  \"repeating-conic-gradient(#d4d4d4 0% 25%, #fff 0% 50%) 50% / 8px 8px\"\n\nconst HUE_GRADIENT =\n  \"linear-gradient(to right, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00)\"\n\nconst thumbClassName =\n  \"block size-4 rounded-full border-2 border-white shadow-[0_0_0_1px_rgb(0_0_0/0.2),0_1px_3px_rgb(0_0_0/0.3)] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n\nfunction areaKeyDelta(key: string) {\n  switch (key) {\n    case \"ArrowLeft\":\n      return { saturation: -1, brightness: 0 }\n    case \"ArrowRight\":\n      return { saturation: 1, brightness: 0 }\n    case \"ArrowUp\":\n      return { saturation: 0, brightness: 1 }\n    case \"ArrowDown\":\n      return { saturation: 0, brightness: -1 }\n    default:\n      return null\n  }\n}\n\nfunction ColorArea({\n  hsva,\n  onHsvaChange,\n  className,\n}: {\n  hsva: Hsva\n  onHsvaChange: (hsva: Hsva) => void\n  className?: string\n}) {\n  const areaRef = React.useRef<HTMLDivElement>(null)\n  const thumbRef = React.useRef<HTMLDivElement>(null)\n\n  function updateFromPointer(event: React.PointerEvent<HTMLDivElement>) {\n    const rect = areaRef.current?.getBoundingClientRect()\n    if (!rect) return\n    onHsvaChange({\n      ...hsva,\n      s: clampUnit((event.clientX - rect.left) / rect.width),\n      v: 1 - clampUnit((event.clientY - rect.top) / rect.height),\n    })\n  }\n\n  function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {\n    const delta = areaKeyDelta(event.key)\n    if (!delta) return\n    event.preventDefault()\n    const step = event.shiftKey ? COARSE_STEP : FINE_STEP\n    onHsvaChange({\n      ...hsva,\n      s: clampUnit(hsva.s + delta.saturation * step),\n      v: clampUnit(hsva.v + delta.brightness * step),\n    })\n  }\n\n  return (\n    <div\n      ref={areaRef}\n      data-slot=\"color-area\"\n      onPointerDown={(event) => {\n        event.preventDefault()\n        event.currentTarget.setPointerCapture(event.pointerId)\n        thumbRef.current?.focus()\n        updateFromPointer(event)\n      }}\n      onPointerMove={(event) => {\n        if (event.currentTarget.hasPointerCapture(event.pointerId))\n          updateFromPointer(event)\n      }}\n      style={{\n        background: `linear-gradient(to top, #000, transparent), linear-gradient(to right, #fff, transparent), hsl(${hsva.h} 100% 50%)`,\n      }}\n      className={cn(\n        \"relative h-40 w-full cursor-crosshair touch-none rounded-md\",\n        className\n      )}\n    >\n      <div\n        ref={thumbRef}\n        role=\"slider\"\n        tabIndex={0}\n        aria-label=\"Saturation and brightness\"\n        aria-valuenow={Math.round(hsva.s * 100)}\n        aria-valuetext={`Saturation ${Math.round(hsva.s * 100)}%, brightness ${Math.round(hsva.v * 100)}%`}\n        onKeyDown={handleKeyDown}\n        style={{\n          left: `${hsva.s * 100}%`,\n          top: `${(1 - hsva.v) * 100}%`,\n          background: formatRgba({ ...hsva, a: 1 }),\n        }}\n        className={cn(thumbClassName, \"absolute -translate-1/2\")}\n      />\n    </div>\n  )\n}\n\nfunction ColorSlider({\n  value,\n  max,\n  step,\n  trackBackground,\n  onValueChange,\n  \"aria-label\": ariaLabel,\n}: {\n  value: number\n  max: number\n  step: number\n  trackBackground: string\n  onValueChange: (value: number) => void\n  \"aria-label\": string\n}) {\n  return (\n    <SliderPrimitive.Root\n      data-slot=\"color-slider\"\n      value={[value]}\n      max={max}\n      step={step}\n      onValueChange={([next]) => onValueChange(next)}\n      className=\"relative flex h-4 w-full touch-none items-center select-none\"\n    >\n      <SliderPrimitive.Track\n        style={{ background: trackBackground }}\n        className=\"relative h-3 w-full grow overflow-hidden rounded-full shadow-[inset_0_0_0_1px_rgb(0_0_0/0.1)]\"\n      />\n      <SliderPrimitive.Thumb\n        aria-label={ariaLabel}\n        className={thumbClassName}\n      />\n    </SliderPrimitive.Root>\n  )\n}\n\nfunction HueSlider({\n  hsva,\n  onHsvaChange,\n}: {\n  hsva: Hsva\n  onHsvaChange: (hsva: Hsva) => void\n}) {\n  return (\n    <ColorSlider\n      aria-label=\"Hue\"\n      value={hsva.h}\n      max={359}\n      step={1}\n      trackBackground={HUE_GRADIENT}\n      onValueChange={(h) => onHsvaChange({ ...hsva, h })}\n    />\n  )\n}\n\nfunction AlphaSlider({\n  hsva,\n  onHsvaChange,\n}: {\n  hsva: Hsva\n  onHsvaChange: (hsva: Hsva) => void\n}) {\n  const opaque = formatRgba({ ...hsva, a: 1 })\n\n  return (\n    <ColorSlider\n      aria-label=\"Opacity\"\n      value={hsva.a}\n      max={1}\n      step={FINE_STEP}\n      trackBackground={`linear-gradient(to right, transparent, ${opaque}), ${CHECKERBOARD}`}\n      onValueChange={(a) => onHsvaChange({ ...hsva, a })}\n    />\n  )\n}\n\nfunction ColorSwatch({\n  color,\n  className,\n  ...props\n}: React.ComponentProps<\"span\"> & { color: string }) {\n  return (\n    <span\n      data-slot=\"color-swatch\"\n      style={{\n        background: `linear-gradient(${color}, ${color}), ${CHECKERBOARD}`,\n      }}\n      className={cn(\n        \"inline-block size-5 shrink-0 rounded-sm shadow-[inset_0_0_0_1px_rgb(0_0_0/0.15)]\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nexport { AlphaSlider, ColorArea, ColorSwatch, HueSlider }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/default/lib/color.ts",
      "content": "export type Hsva = { h: number; s: number; v: number; a: number }\ntype Rgb = { r: number; g: number; b: number }\n\nconst HEX_PATTERN = /^#?([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i\n\nfunction roundTo(value: number, digits: number) {\n  const factor = 10 ** digits\n  return Math.round(value * factor) / factor\n}\n\nfunction toHexByte(channel: number) {\n  return Math.round(channel).toString(16).padStart(2, \"0\")\n}\n\nfunction srgbToLinear(channel: number) {\n  const normalized = channel / 255\n  if (normalized <= 0.04045) return normalized / 12.92\n  return ((normalized + 0.055) / 1.055) ** 2.4\n}\n\nexport function clampUnit(value: number) {\n  return Math.min(1, Math.max(0, value))\n}\n\nexport function hsvaToRgb({ h, s, v }: Hsva): Rgb {\n  const channel = (offset: number) => {\n    const k = (offset + h / 60) % 6\n    return (v - v * s * Math.max(0, Math.min(k, 4 - k, 1))) * 255\n  }\n  return { r: channel(5), g: channel(3), b: channel(1) }\n}\n\nfunction rgbToHsv({ r, g, b }: Rgb) {\n  const [red, green, blue] = [r / 255, g / 255, b / 255]\n  const max = Math.max(red, green, blue)\n  const chroma = max - Math.min(red, green, blue)\n  const saturation = max === 0 ? 0 : chroma / max\n  if (chroma === 0) return { h: 0, s: saturation, v: max }\n\n  let sector = 0\n  switch (max) {\n    case red:\n      sector = ((green - blue) / chroma) % 6\n      break\n    case green:\n      sector = (blue - red) / chroma + 2\n      break\n    case blue:\n      sector = (red - green) / chroma + 4\n      break\n  }\n  return { h: (sector * 60 + 360) % 360, s: saturation, v: max }\n}\n\nexport function parseHex(value: string): Hsva | null {\n  const match = HEX_PATTERN.exec(value.trim())\n  if (!match) return null\n  const digits = match[1]\n  const expanded =\n    digits.length <= 4\n      ? [...digits].map((digit) => digit + digit).join(\"\")\n      : digits\n  const byteAt = (index: number) =>\n    parseInt(expanded.slice(index * 2, index * 2 + 2), 16)\n  const alpha = expanded.length === 8 ? byteAt(3) / 255 : 1\n  return { ...rgbToHsv({ r: byteAt(0), g: byteAt(1), b: byteAt(2) }), a: alpha }\n}\n\nexport function formatHex(hsva: Hsva) {\n  const { r, g, b } = hsvaToRgb(hsva)\n  const alpha = hsva.a < 1 ? toHexByte(hsva.a * 255) : \"\"\n  return `#${toHexByte(r)}${toHexByte(g)}${toHexByte(b)}${alpha}`\n}\n\nexport function formatRgba(hsva: Hsva) {\n  const { r, g, b } = hsvaToRgb(hsva)\n  return `rgb(${Math.round(r)} ${Math.round(g)} ${Math.round(b)} / ${roundTo(hsva.a, 2)})`\n}\n\nexport function formatOklch(hsva: Hsva) {\n  const { r, g, b } = hsvaToRgb(hsva)\n  const [red, green, blue] = [srgbToLinear(r), srgbToLinear(g), srgbToLinear(b)]\n  const long = Math.cbrt(\n    0.4122214708 * red + 0.5363325363 * green + 0.0514459929 * blue\n  )\n  const medium = Math.cbrt(\n    0.2119034982 * red + 0.6806995451 * green + 0.1073969566 * blue\n  )\n  const short = Math.cbrt(\n    0.0883024619 * red + 0.2817188376 * green + 0.6299787005 * blue\n  )\n\n  const lightness =\n    0.2104542553 * long + 0.793617785 * medium - 0.0040720468 * short\n  const greenRed =\n    1.9779984951 * long - 2.428592205 * medium + 0.4505937099 * short\n  const blueYellow =\n    0.0259040371 * long + 0.7827717662 * medium - 0.808675766 * short\n  const chroma = Math.hypot(greenRed, blueYellow)\n  const hue =\n    chroma < 0.0005 ? 0 : (Math.atan2(blueYellow, greenRed) * 180) / Math.PI\n  const alpha = hsva.a < 1 ? ` / ${Math.round(hsva.a * 100)}%` : \"\"\n\n  return `oklch(${roundTo(lightness, 3)} ${roundTo(chroma, 3)} ${roundTo((hue + 360) % 360, 1)}${alpha})`\n}\n",
      "type": "registry:lib"
    }
  ],
  "type": "registry:ui"
}