{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "color-picker",
  "type": "registry:ui",
  "registryDependencies": [],
  "dependencies": ["@base-ui/react", "lucide-react", "motion"],
  "devDependencies": [],
  "files": [
    {
      "path": "color-picker.tsx",
      "content": "\"use client\";\n\nimport { Input as InputPrimitive } from \"@base-ui/react/input\";\nimport { Check, ChevronDown, Copy, Pipette } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport {\n  type ForwardedRef,\n  forwardRef,\n  type HTMLAttributes,\n  type KeyboardEvent as ReactKeyboardEvent,\n  type ReactNode,\n  type RefObject,\n  useCallback,\n  useEffect,\n  useId,\n  useImperativeHandle,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\n\nimport { cn } from \"@/lib/utils\";\n\n// Shared motion tokens — inlined so the registry file is self-contained.\nconst EASE_OUT = [0.16, 1, 0.3, 1] as const;\nconst EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nconst SPRING_PANEL = {\n  type: \"spring\" as const,\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n};\n\nconst controlCornerClassName =\n  \"rounded-lg supports-[corner-shape:squircle]:corner-squircle supports-[corner-shape:squircle]:rounded-[11px]\";\n\nconst controlCornerInheritClassName =\n  \"rounded-[inherit] supports-[corner-shape:squircle]:[corner-shape:inherit]\";\n\nconst FORMAT_OPTIONS = [\"HEX\", \"RGB\", \"HSL\", \"OKLCH\"] as const;\nconst FORMAT_MENU_SOFT_EASE = [0.22, 1, 0.36, 1] as const;\nconst HEX_INPUT_SANITIZE_PATTERN = /[^0-9a-fA-F]/g;\nconst HEX_HASH_PREFIX = /^#/;\nconst HEX_RGB_PATTERN = /^[0-9A-F]{6}$/;\nconst HEX_RGBA_PATTERN = /^[0-9A-F]{8}$/;\n\nconst formatMenuPanelClassName =\n  \"absolute top-full left-0 z-50 mt-1.5 w-28 overflow-hidden border border-[color:var(--color-border)] bg-[color:var(--color-card)] p-1 text-[color:var(--color-card-foreground)] shadow-[0_10px_28px_-20px_rgba(15,23,42,0.28)] dark:shadow-[0_12px_32px_-22px_rgba(0,0,0,0.5)]\";\n\nconst formatMenuItemClassName =\n  \"relative w-full px-3 py-1.5 text-left text-sm outline-none transition-[color,transform] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)]\";\n\nconst formatMenuHighlightClassName =\n  \"absolute inset-0 bg-[color:var(--color-accent)]\";\n\nconst formatMenuHighlightTransition = {\n  type: \"spring\" as const,\n  stiffness: 520,\n  damping: 34,\n  mass: 0.72,\n};\n\nconst formatTriggerTransitionClassName =\n  \"transition-[color,background-color,box-shadow] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)]\";\n\nconst componentThemeClassName =\n  \"[--ic-background:#ffffff] [--ic-foreground:#111111] [--ic-primary:#111111] [--ic-secondary:#646b75] [--ic-surface-border:#e9edf2] [--ic-border:#e3e7ec] [--ic-card:#ffffff] [--ic-card-foreground:#111111] [--ic-muted:#f5f7fa] [--ic-muted-foreground:#6d7480] [--ic-accent:#f3f5f8] [--color-accent:var(--ic-accent)] [--color-accent-foreground:var(--ic-accent-foreground)] [--ic-accent-foreground:#111111] [--ic-input:#e3e7ec] [--ic-ring:rgba(17,17,17,0.16)] [--ic-destructive:#dc2626] [--ic-paper:#fcfcfd] [--ic-popover-foreground:#111111] [--ic-brand:#0ea5e9] [--ic-brand-soft:#bae6fd] [--ic-shadow-soft:0_18px_38px_-24px_rgba(15,23,42,0.35)] [--ic-chart-1:oklch(0.52_0.19_254)] [--ic-chart-2:oklch(0.74_0.11_232)] [--ic-chart-3:oklch(0.42_0.16_262)] [--ic-chart-4:oklch(0.84_0.07_228)] [--ic-chart-5:oklch(0.62_0.14_240)] [--color-background:var(--ic-background)] [--color-foreground:var(--ic-foreground)] [--color-primary:var(--ic-primary)] [--color-secondary:var(--ic-secondary)] [--color-border:var(--ic-border)] [--color-card:var(--ic-card)] [--color-card-foreground:var(--ic-card-foreground)] [--color-muted:var(--ic-muted)] [--color-muted-foreground:var(--ic-muted-foreground)] [--color-accent:var(--ic-accent)] [--color-accent-foreground:var(--ic-accent-foreground)] [--color-input:var(--ic-input)] [--color-ring:var(--ic-ring)] [--color-destructive:var(--ic-destructive)] [--color-paper:var(--ic-paper)] [--color-popover-foreground:var(--ic-popover-foreground)] [--color-brand:var(--ic-brand)] [--color-brand-soft:var(--ic-brand-soft)] [--color-chart-1:var(--ic-chart-1)] [--color-chart-2:var(--ic-chart-2)] [--color-chart-3:var(--ic-chart-3)] [--color-chart-4:var(--ic-chart-4)] [--color-chart-5:var(--ic-chart-5)] dark:[--ic-background:#111111] dark:[--ic-foreground:#f6f3ec] dark:[--ic-primary:#f6f3ec] dark:[--ic-secondary:#cbc6bb] dark:[--ic-surface-border:#2a2a25] dark:[--ic-border:#2b2a25] dark:[--ic-card:#111111] dark:[--ic-card-foreground:#f6f3ec] dark:[--ic-muted:#171716] dark:[--ic-muted-foreground:#9a958a] dark:[--ic-accent:#1a1a18] [--color-accent:var(--ic-accent)] [--color-accent-foreground:var(--ic-accent-foreground)] dark:[--ic-accent-foreground:#f6f3ec] dark:[--ic-input:#2b2a25] dark:[--ic-ring:rgba(246,243,236,0.18)] dark:[--ic-destructive:#f87171] dark:[--ic-paper:#171716] dark:[--ic-popover-foreground:#f6f3ec] dark:[--ic-brand:#38bdf8] dark:[--ic-brand-soft:#0c4a6e] dark:[--ic-shadow-soft:0_20px_44px_-28px_rgba(0,0,0,0.6)] dark:[--ic-chart-1:oklch(0.68_0.17_250)] dark:[--ic-chart-2:oklch(0.82_0.09_225)] dark:[--ic-chart-3:oklch(0.58_0.15_260)] dark:[--ic-chart-4:oklch(0.75_0.12_235)] dark:[--ic-chart-5:oklch(0.88_0.06_220)]\";\n\nconst alphaCheckerClassName =\n  \"bg-[repeating-conic-gradient(var(--color-muted)_0%_25%,transparent_0%_50%)] bg-[length:10px_10px]\";\n\nfunction ColorSwatch({\n  alpha = 100,\n  className,\n  color,\n  shape = \"default\",\n}: {\n  alpha?: number;\n  className?: string;\n  color: string;\n  shape?: ColorPickerSwatchShape;\n}) {\n  const hasTransparency = alpha < 100;\n  const shapeClassName =\n    shape === \"circle\" ? \"rounded-full\" : controlCornerClassName;\n\n  return (\n    <span\n      aria-hidden\n      className={cn(\n        \"border border-black/10\",\n        shapeClassName,\n        hasTransparency && alphaCheckerClassName,\n        className\n      )}\n      style={{ backgroundColor: color }}\n    />\n  );\n}\n\nconst colorPickerShellWidthClassName = \"w-[300px] max-w-full\";\n\nconst popoverTriggerClassName = cn(\n  \"flex h-10 items-center gap-2.5 border border-border bg-card px-3 text-left text-foreground text-sm transition-colors hover:border-foreground/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--color-ring)]\",\n  colorPickerShellWidthClassName,\n  controlCornerClassName\n);\n\nconst swatchTriggerClassName = cn(\n  \"inline-flex shrink-0 transition-transform hover:scale-105 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--color-ring)]\",\n  controlCornerClassName\n);\n\ntype Format = (typeof FORMAT_OPTIONS)[number];\ntype Rgb = { r: number; g: number; b: number };\ntype ParsedHexColor = Rgb & { alpha?: number };\n\nexport type ColorPickerFormat = Format;\n\nexport type ColorPickerSwatchShape = \"default\" | \"circle\";\n\nexport type ColorPickerChangeDetail = {\n  alpha: number;\n  hex: string;\n  hsl: string;\n  oklch: string;\n  rgb: string;\n};\n\nexport interface ColorPickerProps {\n  /** Accessible label when the picker is not labelled by an external element. */\n  \"aria-label\"?: string;\n  /** ID of an external label element. */\n  \"aria-labelledby\"?: string;\n  /** Merged onto the outer picker shell. */\n  className?: string;\n  /** Starting alpha percentage (0–100) when `defaultValue` has no alpha channel. */\n  defaultAlpha?: number;\n  /** Initial color format for the readout row. */\n  defaultFormat?: ColorPickerFormat;\n  /** Starting color for uncontrolled usage (3-, 6-, or 8-digit hex, with or without `#`). */\n  defaultValue?: string;\n  /** Uncontrolled initial open state for `variant=\"popover\"`. */\n  defaultOpen?: boolean;\n  /** Disables interaction and lowers opacity. */\n  disabled?: boolean;\n  /** Root element id. */\n  id?: string;\n  /** Hidden input name for native form submission. */\n  name?: string;\n  /** Called when the selected color changes. Emits `#RRGGBB` or `#RRGGBBAA` when alpha is below 100%. */\n  onChange?: (color: string, detail: ColorPickerChangeDetail) => void;\n  /** Fires when EyeDropper is unavailable (no `window.alert` is shown). */\n  onEyedropperUnsupported?: () => void;\n  /** Controlled open state for `variant=\"popover\"`. */\n  onOpenChange?: (open: boolean) => void;\n  /** Called when the color settles (pointer up, blur, or Enter). */\n  onValueCommit?: (color: string, detail: ColorPickerChangeDetail) => void;\n  /** Controlled open state for `variant=\"popover\"`. */\n  open?: boolean;\n  /** Placeholder for the popover trigger when no color is selected. */\n  placeholder?: string;\n  /** Quick-select swatches shown above the saturation field. */\n  presets?: string[];\n  /** Shows a copy-to-clipboard control in the footer row. */\n  showCopy?: boolean;\n  /** Corner style for swatch triggers and preset chips. */\n  swatchShape?: ColorPickerSwatchShape;\n  /** Hides alpha controls when false and keeps output opaque. */\n  showAlpha?: boolean;\n  /** Hides the pipette control when false. */\n  showEyedropper?: boolean;\n  /** Controlled hex color (3-, 6-, or 8-digit, with or without `#`). */\n  value?: string;\n  /** Inline panel, field popover trigger, or swatch popover trigger. */\n  variant?: \"inline\" | \"popover\" | \"swatch\";\n}\n\n/** @deprecated Use ColorPicker instead. */\nexport type FluidColorPickerProps = ColorPickerProps;\n\nfunction getFormatMenuMotion() {\n  return {\n    animate: { opacity: 1, scale: 1, y: 0 },\n    exit: { opacity: 0, scale: 0.96, y: -6 },\n    initial: { opacity: 0, scale: 0.94, y: -8 },\n    transition: { duration: 0.26, ease: FORMAT_MENU_SOFT_EASE },\n  };\n}\n\nfunction getFormatItemMotion(index: number) {\n  return {\n    animate: { opacity: 1, x: 0 },\n    initial: { opacity: 0, x: -8 },\n    transition: {\n      delay: 0.04 + index * 0.035,\n      duration: 0.22,\n      ease: FORMAT_MENU_SOFT_EASE,\n    },\n  };\n}\n\nfunction hsvToRgb(h: number, s: number, v: number) {\n  const saturation = s / 100;\n  const brightness = v / 100;\n  const chroma = brightness * saturation;\n  const intermediate = chroma * (1 - Math.abs(((h / 60) % 2) - 1));\n  const minimum = brightness - chroma;\n  let red = 0;\n  let green = 0;\n  let blue = 0;\n\n  if (h < 60) {\n    red = chroma;\n    green = intermediate;\n  } else if (h < 120) {\n    red = intermediate;\n    green = chroma;\n  } else if (h < 180) {\n    green = chroma;\n    blue = intermediate;\n  } else if (h < 240) {\n    green = intermediate;\n    blue = chroma;\n  } else if (h < 300) {\n    red = intermediate;\n    blue = chroma;\n  } else {\n    red = chroma;\n    blue = intermediate;\n  }\n\n  return {\n    r: Math.round((red + minimum) * 255),\n    g: Math.round((green + minimum) * 255),\n    b: Math.round((blue + minimum) * 255),\n  };\n}\n\nfunction toHex(value: number) {\n  return value.toString(16).padStart(2, \"0\").toUpperCase();\n}\n\nfunction rgbToHex(red: number, green: number, blue: number) {\n  return `${toHex(red)}${toHex(green)}${toHex(blue)}`;\n}\n\nfunction rgbToHsv(red: number, green: number, blue: number) {\n  const normalizedRed = red / 255;\n  const normalizedGreen = green / 255;\n  const normalizedBlue = blue / 255;\n  const max = Math.max(normalizedRed, normalizedGreen, normalizedBlue);\n  const min = Math.min(normalizedRed, normalizedGreen, normalizedBlue);\n  const delta = max - min;\n  let hue = 0;\n\n  if (delta !== 0) {\n    if (max === normalizedRed) {\n      hue = ((normalizedGreen - normalizedBlue) / delta) % 6;\n    } else if (max === normalizedGreen) {\n      hue = (normalizedBlue - normalizedRed) / delta + 2;\n    } else {\n      hue = (normalizedRed - normalizedGreen) / delta + 4;\n    }\n    hue *= 60;\n    if (hue < 0) {\n      hue += 360;\n    }\n  }\n\n  return {\n    h: hue,\n    s: max === 0 ? 0 : (delta / max) * 100,\n    v: max * 100,\n  };\n}\n\nfunction rgbToHsl(red: number, green: number, blue: number) {\n  const normalizedRed = red / 255;\n  const normalizedGreen = green / 255;\n  const normalizedBlue = blue / 255;\n  const max = Math.max(normalizedRed, normalizedGreen, normalizedBlue);\n  const min = Math.min(normalizedRed, normalizedGreen, normalizedBlue);\n  const lightness = (max + min) / 2;\n  let hue = 0;\n  let saturation = 0;\n\n  if (max !== min) {\n    const delta = max - min;\n    saturation =\n      lightness > 0.5 ? delta / (2 - max - min) : delta / (max + min);\n    if (max === normalizedRed) {\n      hue = ((normalizedGreen - normalizedBlue) / delta) % 6;\n    } else if (max === normalizedGreen) {\n      hue = (normalizedBlue - normalizedRed) / delta + 2;\n    } else {\n      hue = (normalizedRed - normalizedGreen) / delta + 4;\n    }\n    hue *= 60;\n    if (hue < 0) {\n      hue += 360;\n    }\n  }\n\n  return {\n    h: Math.round(hue),\n    s: Math.round(saturation * 100),\n    l: Math.round(lightness * 100),\n  };\n}\n\nfunction srgbToLinear(channel: number) {\n  const normalized = channel / 255;\n  return normalized <= 0.040_45\n    ? normalized / 12.92\n    : ((normalized + 0.055) / 1.055) ** 2.4;\n}\n\nfunction rgbToOklch(red: number, green: number, blue: number) {\n  const linearRed = srgbToLinear(red);\n  const linearGreen = srgbToLinear(green);\n  const linearBlue = srgbToLinear(blue);\n  const lightnessCandidate =\n    0.412_221_470_8 * linearRed +\n    0.536_332_536_3 * linearGreen +\n    0.051_445_992_9 * linearBlue;\n  const mediumCandidate =\n    0.211_903_498_2 * linearRed +\n    0.680_699_545_1 * linearGreen +\n    0.107_396_956_6 * linearBlue;\n  const shortCandidate =\n    0.088_302_461_9 * linearRed +\n    0.281_718_837_6 * linearGreen +\n    0.629_978_700_5 * linearBlue;\n  const lightness =\n    0.210_454_255_3 * lightnessCandidate ** (1 / 3) +\n    0.793_617_785 * mediumCandidate ** (1 / 3) -\n    0.004_072_046_8 * shortCandidate ** (1 / 3);\n  const axisA =\n    1.977_998_495_1 * lightnessCandidate ** (1 / 3) -\n    2.428_592_205 * mediumCandidate ** (1 / 3) +\n    0.450_593_709_9 * shortCandidate ** (1 / 3);\n  const axisB =\n    0.025_904_037_1 * lightnessCandidate ** (1 / 3) +\n    0.782_771_766_2 * mediumCandidate ** (1 / 3) -\n    0.808_675_766 * shortCandidate ** (1 / 3);\n  const chroma = Math.hypot(axisA, axisB);\n  let hue = (Math.atan2(axisB, axisA) * 180) / Math.PI;\n  if (hue < 0) {\n    hue += 360;\n  }\n\n  return { l: lightness, c: chroma, h: hue };\n}\n\nfunction clampPercent(value: number) {\n  return Math.max(0, Math.min(100, Math.round(value)));\n}\n\nfunction normalizeHue(hue: number) {\n  const normalized = hue % 360;\n  return normalized < 0 ? normalized + 360 : normalized;\n}\n\nfunction clampChannel(value: number, min = 0, max = 255) {\n  return Math.max(min, Math.min(max, Math.round(value)));\n}\n\nfunction linearToSrgb(channel: number) {\n  const absolute = Math.abs(channel);\n\n  if (absolute > 0.003_130_8) {\n    return Math.sign(channel) * (1.055 * absolute ** (1 / 2.4) - 0.055);\n  }\n\n  return 12.92 * channel;\n}\n\nfunction hslToRgb(hue: number, saturation: number, lightness: number) {\n  const saturationNormalized = saturation / 100;\n  const lightnessNormalized = lightness / 100;\n  const chroma =\n    (1 - Math.abs(2 * lightnessNormalized - 1)) * saturationNormalized;\n  const intermediate = chroma * (1 - Math.abs(((hue / 60) % 2) - 1));\n  const minimum = lightnessNormalized - chroma / 2;\n  let red = 0;\n  let green = 0;\n  let blue = 0;\n\n  if (hue < 60) {\n    red = chroma;\n    green = intermediate;\n  } else if (hue < 120) {\n    red = intermediate;\n    green = chroma;\n  } else if (hue < 180) {\n    green = chroma;\n    blue = intermediate;\n  } else if (hue < 240) {\n    green = intermediate;\n    blue = chroma;\n  } else if (hue < 300) {\n    red = intermediate;\n    blue = chroma;\n  } else {\n    red = chroma;\n    blue = intermediate;\n  }\n\n  return {\n    r: clampChannel((red + minimum) * 255),\n    g: clampChannel((green + minimum) * 255),\n    b: clampChannel((blue + minimum) * 255),\n  };\n}\n\nfunction oklchToRgb(lightness: number, chroma: number, hue: number) {\n  const hueRadians = (hue * Math.PI) / 180;\n  const axisA = chroma * Math.cos(hueRadians);\n  const axisB = chroma * Math.sin(hueRadians);\n  const lightnessPrime =\n    lightness + 0.396_337_777_4 * axisA + 0.215_803_757_3 * axisB;\n  const mediumPrime =\n    lightness - 0.105_561_345_8 * axisA - 0.063_854_172_8 * axisB;\n  const shortPrime =\n    lightness - 0.089_484_177_5 * axisA - 1.291_485_548 * axisB;\n  const linearRed =\n    4.076_741_662_1 * lightnessPrime ** 3 -\n    3.307_711_591_3 * mediumPrime ** 3 +\n    0.230_969_929_2 * shortPrime ** 3;\n  const linearGreen =\n    -1.268_438_004_6 * lightnessPrime ** 3 +\n    2.609_757_401_1 * mediumPrime ** 3 -\n    0.341_319_396_5 * shortPrime ** 3;\n  const linearBlue =\n    -0.004_196_086_3 * lightnessPrime ** 3 -\n    0.703_418_614_7 * mediumPrime ** 3 +\n    1.707_614_701 * shortPrime ** 3;\n\n  return {\n    r: clampChannel(linearToSrgb(linearRed) * 255),\n    g: clampChannel(linearToSrgb(linearGreen) * 255),\n    b: clampChannel(linearToSrgb(linearBlue) * 255),\n  };\n}\n\nfunction expandShortHex(normalized: string) {\n  if (normalized.length === 3) {\n    return normalized\n      .split(\"\")\n      .map((character) => `${character}${character}`)\n      .join(\"\");\n  }\n\n  if (normalized.length === 4) {\n    return normalized\n      .split(\"\")\n      .map((character) => `${character}${character}`)\n      .join(\"\");\n  }\n\n  return normalized;\n}\n\nfunction parseHexColor(input?: string): ParsedHexColor | null {\n  if (!input) {\n    return null;\n  }\n\n  const normalized = expandShortHex(\n    input.trim().replace(HEX_HASH_PREFIX, \"\").toUpperCase()\n  );\n\n  if (HEX_RGB_PATTERN.test(normalized)) {\n    return {\n      r: Number.parseInt(normalized.slice(0, 2), 16),\n      g: Number.parseInt(normalized.slice(2, 4), 16),\n      b: Number.parseInt(normalized.slice(4, 6), 16),\n    };\n  }\n\n  if (HEX_RGBA_PATTERN.test(normalized)) {\n    const alphaChannel = Number.parseInt(normalized.slice(6, 8), 16);\n\n    return {\n      r: Number.parseInt(normalized.slice(0, 2), 16),\n      g: Number.parseInt(normalized.slice(2, 4), 16),\n      b: Number.parseInt(normalized.slice(4, 6), 16),\n      alpha: Math.round((alphaChannel / 255) * 100),\n    };\n  }\n\n  return null;\n}\n\nfunction formatColorValue(rgb: Rgb, alpha: number) {\n  const hex = `#${rgbToHex(rgb.r, rgb.g, rgb.b)}`;\n\n  if (alpha >= 100) {\n    return hex;\n  }\n\n  const alphaChannel = Math.max(\n    0,\n    Math.min(255, Math.round((alpha / 100) * 255))\n  );\n\n  return `${hex}${toHex(alphaChannel)}`;\n}\n\nfunction formatRgbString(rgb: Rgb, alpha: number) {\n  if (alpha >= 100) {\n    return `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`;\n  }\n\n  const normalizedAlpha = Math.round((alpha / 100) * 100) / 100;\n  return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${normalizedAlpha})`;\n}\n\nfunction formatHslString(rgb: Rgb, alpha: number) {\n  const { h, s, l } = rgbToHsl(rgb.r, rgb.g, rgb.b);\n\n  if (alpha >= 100) {\n    return `hsl(${h}, ${s}%, ${l}%)`;\n  }\n\n  const normalizedAlpha = Math.round((alpha / 100) * 100) / 100;\n  return `hsla(${h}, ${s}%, ${l}%, ${normalizedAlpha})`;\n}\n\nfunction formatOklchString(rgb: Rgb, alpha: number) {\n  const { l, c, h } = rgbToOklch(rgb.r, rgb.g, rgb.b);\n  const roundedLightness = Math.round(l * 1000) / 1000;\n  const roundedChroma = Math.round(c * 1000) / 1000;\n  const roundedHue = Math.round(h);\n\n  if (alpha >= 100) {\n    return `oklch(${roundedLightness} ${roundedChroma} ${roundedHue})`;\n  }\n\n  const normalizedAlpha = Math.round((alpha / 100) * 100) / 100;\n  return `oklch(${roundedLightness} ${roundedChroma} ${roundedHue} / ${normalizedAlpha})`;\n}\n\nfunction buildChangeDetail(rgb: Rgb, alpha: number): ColorPickerChangeDetail {\n  return {\n    alpha,\n    hex: formatColorValue(rgb, alpha),\n    hsl: formatHslString(rgb, alpha),\n    oklch: formatOklchString(rgb, alpha),\n    rgb: formatRgbString(rgb, alpha),\n  };\n}\n\nfunction getInitialAlpha(color: string | undefined, fallbackAlpha: number) {\n  const parsed = parseHexColor(color);\n\n  if (parsed?.alpha !== undefined) {\n    return clampPercent(parsed.alpha);\n  }\n\n  return clampPercent(fallbackAlpha);\n}\n\nconst DEFAULT_RGB: Rgb = { r: 59, g: 130, b: 246 };\n\nfunction getInitialRgb(color?: string): Rgb {\n  const parsed = parseHexColor(color);\n  if (!parsed) {\n    return DEFAULT_RGB;\n  }\n\n  return { r: parsed.r, g: parsed.g, b: parsed.b };\n}\n\nfunction normalizeColorValue(color: string, alphaFallback = 100) {\n  const parsed = parseHexColor(color);\n\n  if (!parsed) {\n    return null;\n  }\n\n  const { alpha, ...rgb } = parsed;\n\n  return formatColorValue(rgb, alpha ?? alphaFallback);\n}\n\nfunction getPickerHsv(rgb: Rgb) {\n  const { h, s, v } = rgbToHsv(rgb.r, rgb.g, rgb.b);\n\n  return {\n    h,\n    s: Math.round(s),\n    v: Math.round(v),\n  };\n}\n\nfunction getMaxOklchChroma(lightness: number, hue: number) {\n  let max = 0;\n\n  for (let chroma = 0; chroma <= 0.5; chroma += 0.002) {\n    const rgb = oklchToRgb(lightness, chroma, hue);\n    const roundTrip = rgbToOklch(rgb.r, rgb.g, rgb.b);\n\n    if (roundTrip.c >= chroma - 0.006) {\n      max = chroma;\n    } else {\n      break;\n    }\n  }\n\n  return Math.max(0.001, Math.round(max * 1000) / 1000);\n}\n\nfunction getDisplayHex(rgb: Rgb, alpha: number, showAlpha: boolean) {\n  const hex = rgbToHex(rgb.r, rgb.g, rgb.b);\n\n  if (!showAlpha || alpha >= 100) {\n    return hex;\n  }\n\n  const alphaChannel = Math.max(\n    0,\n    Math.min(255, Math.round((alpha / 100) * 255))\n  );\n\n  return `${hex}${toHex(alphaChannel)}`;\n}\n\ntype ColorEmitMode = \"none\" | \"change\" | \"commit\";\n\nfunction buildNormalizedColor(\n  nextRgb: Rgb,\n  nextAlpha: number,\n  showAlpha: boolean\n) {\n  const resolvedAlpha = showAlpha ? clampPercent(nextAlpha) : 100;\n  const normalized = formatColorValue(nextRgb, resolvedAlpha);\n  const detail = buildChangeDetail(nextRgb, resolvedAlpha);\n\n  return { detail, normalized, resolvedAlpha };\n}\n\nfunction useControllableOpen(\n  openProp: boolean | undefined,\n  defaultOpen: boolean,\n  onOpenChange?: (open: boolean) => void\n) {\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const isControlled = openProp !== undefined;\n  const open = isControlled ? openProp : internalOpen;\n\n  const setOpen = useCallback(\n    (next: boolean) => {\n      if (!isControlled) {\n        setInternalOpen(next);\n      }\n\n      onOpenChange?.(next);\n    },\n    [isControlled, onOpenChange]\n  );\n\n  return [open, setOpen] as const;\n}\n\ntype ColorPickerController = ReturnType<typeof useColorPickerController>;\n\nfunction useColorPickerController({\n  defaultAlpha = 100,\n  defaultFormat = \"HEX\",\n  defaultValue = \"#3B82F6\",\n  onChange,\n  onValueCommit,\n  showAlpha = true,\n  value,\n}: Pick<\n  ColorPickerProps,\n  | \"defaultAlpha\"\n  | \"defaultFormat\"\n  | \"defaultValue\"\n  | \"onChange\"\n  | \"onValueCommit\"\n  | \"showAlpha\"\n  | \"value\"\n>) {\n  const resolvedDefaultAlpha = clampPercent(defaultAlpha);\n  const initialRgb = getInitialRgb(value ?? defaultValue);\n  const initialAlpha = showAlpha\n    ? getInitialAlpha(value ?? defaultValue, resolvedDefaultAlpha)\n    : 100;\n  const initialPickerHsv = getPickerHsv(initialRgb);\n  const [rgb, setRgb] = useState<Rgb>(initialRgb);\n  const [pickerHue, setPickerHue] = useState(initialPickerHsv.h);\n  const [pickerSat, setPickerSat] = useState(initialPickerHsv.s);\n  const [pickerVal, setPickerVal] = useState(initialPickerHsv.v);\n  const [alpha, setAlpha] = useState(initialAlpha);\n  const [format, setFormat] = useState<Format>(defaultFormat);\n  const [hexInput, setHexInput] = useState(\"\");\n  const [hexFocused, setHexFocused] = useState(false);\n  const [hexInvalid, setHexInvalid] = useState(false);\n  const onChangeRef = useRef(onChange);\n  const onValueCommitRef = useRef(onValueCommit);\n  const rgbRef = useRef(initialRgb);\n  const pickerHueRef = useRef(initialPickerHsv.h);\n  const pickerSatRef = useRef(initialPickerHsv.s);\n  const pickerValRef = useRef(initialPickerHsv.v);\n  const alphaRef = useRef(initialAlpha);\n  const showAlphaRef = useRef(showAlpha);\n  const lastSyncedColorRef = useRef(\n    normalizeColorValue(\n      value ?? defaultValue,\n      showAlpha ? resolvedDefaultAlpha : 100\n    ) ?? formatColorValue(initialRgb, initialAlpha)\n  );\n  const lastNotifiedColorRef = useRef(lastSyncedColorRef.current);\n  const lastCommittedColorRef = useRef(lastSyncedColorRef.current);\n  const isEditingRef = useRef(false);\n  const isDraggingSvRef = useRef(false);\n  const isDraggingHueRef = useRef(false);\n  const isDraggingAlphaRef = useRef(false);\n\n  onChangeRef.current = onChange;\n  onValueCommitRef.current = onValueCommit;\n  rgbRef.current = rgb;\n  alphaRef.current = alpha;\n  pickerHueRef.current = pickerHue;\n  pickerSatRef.current = pickerSat;\n  pickerValRef.current = pickerVal;\n  showAlphaRef.current = showAlpha;\n\n  const effectiveAlpha = showAlpha ? alpha : 100;\n  const colorValue = formatColorValue(rgb, effectiveAlpha);\n  const hex = rgbToHex(rgb.r, rgb.g, rgb.b);\n  const hueColor = hsvToRgb(pickerHue, 100, 100);\n  const hueHex = rgbToHex(hueColor.r, hueColor.g, hueColor.b);\n  const hsl = useMemo(\n    () => rgbToHsl(rgb.r, rgb.g, rgb.b),\n    [rgb.b, rgb.g, rgb.r]\n  );\n  const oklch = useMemo(\n    () => rgbToOklch(rgb.r, rgb.g, rgb.b),\n    [rgb.b, rgb.g, rgb.r]\n  );\n  const oklchMaxChroma = useMemo(\n    () => getMaxOklchChroma(oklch.l, oklch.h),\n    [oklch.h, oklch.l]\n  );\n\n  const syncPickerFromRgb = useCallback((nextRgb: Rgb) => {\n    const nextPickerHsv = getPickerHsv(nextRgb);\n    const nextHue =\n      nextPickerHsv.s === 0 ? pickerHueRef.current : nextPickerHsv.h;\n\n    pickerHueRef.current = nextHue;\n    pickerSatRef.current = nextPickerHsv.s;\n    pickerValRef.current = nextPickerHsv.v;\n    setPickerHue(nextHue);\n    setPickerSat(nextPickerHsv.s);\n    setPickerVal(nextPickerHsv.v);\n  }, []);\n\n  const notifyChange = useCallback(\n    (nextRgb: Rgb, nextAlpha = alphaRef.current) => {\n      const { detail, normalized } = buildNormalizedColor(\n        nextRgb,\n        nextAlpha,\n        showAlphaRef.current\n      );\n\n      if (normalized === lastNotifiedColorRef.current) {\n        return;\n      }\n\n      lastNotifiedColorRef.current = normalized;\n      lastSyncedColorRef.current = normalized;\n      onChangeRef.current?.(normalized, detail);\n    },\n    []\n  );\n\n  const commitChange = useCallback(\n    (nextRgb: Rgb, nextAlpha = alphaRef.current) => {\n      const { detail, normalized } = buildNormalizedColor(\n        nextRgb,\n        nextAlpha,\n        showAlphaRef.current\n      );\n\n      if (normalized !== lastNotifiedColorRef.current) {\n        lastNotifiedColorRef.current = normalized;\n        lastSyncedColorRef.current = normalized;\n        onChangeRef.current?.(normalized, detail);\n      }\n\n      if (normalized !== lastCommittedColorRef.current) {\n        lastCommittedColorRef.current = normalized;\n        onValueCommitRef.current?.(normalized, detail);\n      }\n    },\n    []\n  );\n\n  const emitColor = useCallback(\n    (\n      nextRgb: Rgb,\n      nextAlpha = alphaRef.current,\n      mode: ColorEmitMode = \"commit\"\n    ) => {\n      if (mode === \"none\") {\n        return;\n      }\n\n      if (mode === \"change\") {\n        notifyChange(nextRgb, nextAlpha);\n        return;\n      }\n\n      commitChange(nextRgb, nextAlpha);\n    },\n    [commitChange, notifyChange]\n  );\n\n  const applyColorState = useCallback(\n    (\n      nextRgb: Rgb,\n      nextAlpha: number,\n      {\n        emitMode = \"none\",\n        syncPicker = true,\n      }: { emitMode?: ColorEmitMode; syncPicker?: boolean } = {}\n    ) => {\n      const resolvedAlpha = showAlphaRef.current\n        ? clampPercent(nextAlpha)\n        : 100;\n\n      rgbRef.current = nextRgb;\n      setRgb(nextRgb);\n\n      if (syncPicker) {\n        syncPickerFromRgb(nextRgb);\n      }\n\n      alphaRef.current = resolvedAlpha;\n      setAlpha(resolvedAlpha);\n\n      emitColor(nextRgb, resolvedAlpha, emitMode);\n    },\n    [emitColor, syncPickerFromRgb]\n  );\n\n  const applyAlpha = useCallback(\n    (nextAlpha: number, emitMode: ColorEmitMode = \"none\") => {\n      if (!showAlphaRef.current) {\n        return;\n      }\n\n      const clampedAlpha = clampPercent(nextAlpha);\n\n      alphaRef.current = clampedAlpha;\n      setAlpha(clampedAlpha);\n\n      emitColor(rgbRef.current, clampedAlpha, emitMode);\n    },\n    [emitColor]\n  );\n\n  const applyRgb = useCallback(\n    (nextRgb: Rgb, emitMode: ColorEmitMode = \"none\", syncPicker = true) => {\n      rgbRef.current = nextRgb;\n      setRgb(nextRgb);\n\n      if (syncPicker) {\n        syncPickerFromRgb(nextRgb);\n      }\n\n      emitColor(nextRgb, alphaRef.current, emitMode);\n    },\n    [emitColor, syncPickerFromRgb]\n  );\n\n  const applyPickerHsv = useCallback(\n    (\n      nextHue: number,\n      nextSaturation: number,\n      nextValue: number,\n      emitMode: ColorEmitMode = \"none\"\n    ) => {\n      const normalizedHue = normalizeHue(nextHue);\n      const roundedSaturation = Math.max(\n        0,\n        Math.min(100, Math.round(nextSaturation))\n      );\n      const roundedValue = Math.max(0, Math.min(100, Math.round(nextValue)));\n      const nextRgb = hsvToRgb(normalizedHue, roundedSaturation, roundedValue);\n\n      pickerHueRef.current = normalizedHue;\n      pickerSatRef.current = roundedSaturation;\n      pickerValRef.current = roundedValue;\n      setPickerHue(normalizedHue);\n      setPickerSat(roundedSaturation);\n      setPickerVal(roundedValue);\n      applyRgb(nextRgb, emitMode, false);\n    },\n    [applyRgb]\n  );\n\n  const setFromHex = useCallback(\n    (nextValue: string, emitMode: ColorEmitMode = \"commit\") => {\n      const parsed = parseHexColor(nextValue);\n\n      if (!parsed) {\n        return false;\n      }\n\n      const { alpha: parsedAlpha, ...nextRgb } = parsed;\n      const nextAlpha =\n        parsedAlpha !== undefined && showAlphaRef.current\n          ? parsedAlpha\n          : showAlphaRef.current\n            ? alphaRef.current\n            : 100;\n\n      applyColorState(nextRgb, nextAlpha, { emitMode, syncPicker: true });\n      return true;\n    },\n    [applyColorState]\n  );\n\n  const setFromHsl = useCallback(\n    (nextHue: number, nextSaturation: number, nextLightness: number) => {\n      applyRgb(hslToRgb(nextHue, nextSaturation, nextLightness), \"commit\");\n    },\n    [applyRgb]\n  );\n\n  const setFromOklch = useCallback(\n    (nextLightness: number, nextChroma: number, nextHue: number) => {\n      applyRgb(oklchToRgb(nextLightness, nextChroma, nextHue), \"commit\");\n    },\n    [applyRgb]\n  );\n\n  const commitHexInput = useCallback(\n    (rawValue: string, emitMode: ColorEmitMode = \"commit\") => {\n      const normalized = rawValue\n        .trim()\n        .replace(HEX_HASH_PREFIX, \"\")\n        .toUpperCase();\n\n      if (normalized.length === 8) {\n        return setFromHex(normalized, emitMode);\n      }\n\n      if (normalized.length === 6) {\n        return setFromHex(normalized, emitMode);\n      }\n\n      if (normalized.length === 3 || normalized.length === 4) {\n        return setFromHex(expandShortHex(normalized), emitMode);\n      }\n\n      return false;\n    },\n    [setFromHex]\n  );\n\n  useEffect(() => {\n    if (\n      !value ||\n      isEditingRef.current ||\n      hexFocused ||\n      isDraggingSvRef.current ||\n      isDraggingHueRef.current ||\n      isDraggingAlphaRef.current\n    ) {\n      return;\n    }\n\n    const normalizedValue = normalizeColorValue(\n      value,\n      showAlpha ? resolvedDefaultAlpha : 100\n    );\n\n    if (!normalizedValue || normalizedValue === lastSyncedColorRef.current) {\n      return;\n    }\n\n    const parsed = parseHexColor(normalizedValue);\n\n    if (!parsed) {\n      return;\n    }\n\n    const { alpha: parsedAlpha, ...nextRgb } = parsed;\n    const nextAlpha = showAlpha ? (parsedAlpha ?? resolvedDefaultAlpha) : 100;\n\n    lastSyncedColorRef.current = normalizedValue;\n    lastNotifiedColorRef.current = normalizedValue;\n    lastCommittedColorRef.current = normalizedValue;\n    rgbRef.current = nextRgb;\n    alphaRef.current = nextAlpha;\n    setRgb(nextRgb);\n    setAlpha(nextAlpha);\n    syncPickerFromRgb(nextRgb);\n  }, [hexFocused, resolvedDefaultAlpha, showAlpha, syncPickerFromRgb, value]);\n\n  useEffect(() => {\n    if (!showAlpha && alpha !== 100) {\n      applyAlpha(100, \"none\");\n    }\n  }, [alpha, applyAlpha, showAlpha]);\n\n  return {\n    alpha,\n    applyAlpha,\n    applyPickerHsv,\n    applyRgb,\n    colorValue,\n    commitChange,\n    commitHexInput,\n    effectiveAlpha,\n    format,\n    hex,\n    hexFocused,\n    hexInput,\n    hexInvalid,\n    hsl,\n    hueHex,\n    isDraggingAlphaRef,\n    isDraggingHueRef,\n    isDraggingSvRef,\n    isEditingRef,\n    oklch,\n    oklchMaxChroma,\n    pickerHue,\n    pickerSat,\n    pickerVal,\n    rgb,\n    setFormat,\n    setFromHsl,\n    setFromOklch,\n    setFromHex,\n    setHexFocused,\n    setHexInput,\n    setHexInvalid,\n  };\n}\n\nfunction PresetSwatches({\n  disabled,\n  onSelect,\n  presets,\n  swatchShape = \"default\",\n}: {\n  disabled?: boolean;\n  onSelect: (color: string) => void;\n  presets: string[];\n  swatchShape?: ColorPickerSwatchShape;\n}) {\n  if (presets.length === 0) {\n    return null;\n  }\n\n  const validPresets = presets.filter(\n    (preset) => parseHexColor(preset) !== null\n  );\n\n  if (validPresets.length === 0) {\n    return null;\n  }\n\n  return (\n    <ul\n      aria-label=\"Color presets\"\n      className=\"mb-3 flex list-none flex-wrap gap-1.5 p-0\"\n    >\n      {validPresets.map((preset, index) => {\n        const parsed = parseHexColor(preset);\n        const swatchAlpha = parsed?.alpha ?? 100;\n        const swatchColor = parsed\n          ? formatColorValue(\n              { r: parsed.r, g: parsed.g, b: parsed.b },\n              swatchAlpha\n            )\n          : preset;\n\n        return (\n          <li key={`${preset}-${index}`}>\n            <button\n              aria-label={`Select ${swatchColor}`}\n              className={cn(\n                \"transition-transform hover:scale-105 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--color-ring)]\",\n                controlCornerClassName\n              )}\n              disabled={disabled}\n              onClick={() => onSelect(preset)}\n              type=\"button\"\n            >\n              <ColorSwatch\n                alpha={swatchAlpha}\n                className=\"block h-6 w-6 shadow-sm\"\n                color={swatchColor}\n                shape={swatchShape}\n              />\n            </button>\n          </li>\n        );\n      })}\n    </ul>\n  );\n}\n\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: panel coordinates SV/hue/alpha drags, format menu, and readouts in one shell.\nfunction ColorPickerPanel({\n  ariaLabel,\n  ariaLabelledBy,\n  controller,\n  disabled,\n  fillWidth = false,\n  formatMenuId,\n  formatTriggerId,\n  id,\n  onEyedropperUnsupported,\n  panelRef,\n  presets,\n  showAlpha,\n  showCopy,\n  showEyedropper,\n  svFieldId,\n  swatchShape = \"default\",\n}: {\n  ariaLabel?: string;\n  ariaLabelledBy?: string;\n  controller: ColorPickerController;\n  disabled?: boolean;\n  fillWidth?: boolean;\n  formatMenuId: string;\n  formatTriggerId: string;\n  id?: string;\n  onEyedropperUnsupported?: () => void;\n  panelRef?: RefObject<HTMLFieldSetElement | null>;\n  presets?: string[];\n  showAlpha: boolean;\n  showCopy: boolean;\n  showEyedropper: boolean;\n  svFieldId: string;\n  swatchShape?: ColorPickerSwatchShape;\n}) {\n  const {\n    alpha,\n    applyAlpha,\n    applyPickerHsv,\n    applyRgb,\n    colorValue,\n    commitChange,\n    commitHexInput,\n    effectiveAlpha,\n    format,\n    hexFocused,\n    hexInput,\n    hexInvalid,\n    hsl,\n    hueHex,\n    isDraggingAlphaRef,\n    isDraggingHueRef,\n    isDraggingSvRef,\n    isEditingRef,\n    oklch,\n    oklchMaxChroma,\n    pickerHue,\n    pickerSat,\n    pickerVal,\n    rgb,\n    setFormat,\n    setFromHsl,\n    setFromOklch,\n    setFromHex,\n    setHexFocused,\n    setHexInput,\n    setHexInvalid,\n  } = controller;\n\n  const { r, g, b } = rgb;\n  const svRef = useRef<HTMLDivElement>(null);\n  const dragRef = useRef<null | \"sv\" | \"hue\" | \"alpha\">(null);\n  const formatMenuRef = useRef<HTMLDivElement>(null);\n  const rgbRef = useRef(rgb);\n  const alphaRef = useRef(alpha);\n  const pickerHueRef = useRef(pickerHue);\n  const pickerSatRef = useRef(pickerSat);\n  const pickerValRef = useRef(pickerVal);\n  const [formatOpen, setFormatOpen] = useState(false);\n  const [hoveredFormat, setHoveredFormat] = useState<Format | null>(null);\n  const [copied, setCopied] = useState(false);\n  const copyTimeoutRef = useRef<number | null>(null);\n  const formatMenuMotion = getFormatMenuMotion();\n\n  useEffect(\n    () => () => {\n      if (copyTimeoutRef.current !== null) {\n        window.clearTimeout(copyTimeoutRef.current);\n      }\n    },\n    []\n  );\n\n  rgbRef.current = rgb;\n  alphaRef.current = alpha;\n  pickerHueRef.current = pickerHue;\n  pickerSatRef.current = pickerSat;\n  pickerValRef.current = pickerVal;\n\n  const updateSV = useCallback(\n    (\n      event: { clientX: number; clientY: number },\n      emitMode: ColorEmitMode = \"none\"\n    ) => {\n      const element = svRef.current;\n\n      if (!element) {\n        return;\n      }\n\n      const rect = element.getBoundingClientRect();\n\n      if (rect.width <= 0 || rect.height <= 0) {\n        return;\n      }\n\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\n      applyPickerHsv(\n        pickerHueRef.current,\n        Math.round(x * 100),\n        Math.round((1 - y) * 100),\n        emitMode\n      );\n    },\n    [applyPickerHsv]\n  );\n\n  useEffect(() => {\n    const move = (event: PointerEvent) => {\n      if (dragRef.current === \"sv\") {\n        updateSV(event, \"none\");\n      }\n    };\n    const up = () => {\n      if (\n        dragRef.current === \"sv\" ||\n        dragRef.current === \"hue\" ||\n        dragRef.current === \"alpha\"\n      ) {\n        commitChange(rgbRef.current, alphaRef.current);\n      }\n\n      dragRef.current = null;\n      isDraggingSvRef.current = false;\n      isDraggingHueRef.current = false;\n      isDraggingAlphaRef.current = false;\n    };\n\n    window.addEventListener(\"pointermove\", move);\n    window.addEventListener(\"pointerup\", up);\n    window.addEventListener(\"pointercancel\", up);\n\n    return () => {\n      window.removeEventListener(\"pointermove\", move);\n      window.removeEventListener(\"pointerup\", up);\n      window.removeEventListener(\"pointercancel\", up);\n    };\n  }, [\n    commitChange,\n    isDraggingAlphaRef,\n    isDraggingHueRef,\n    isDraggingSvRef,\n    updateSV,\n  ]);\n\n  useEffect(() => {\n    if (!formatOpen) {\n      return;\n    }\n\n    const handlePointerDown = (event: PointerEvent) => {\n      const target = event.target as Node;\n\n      if (formatMenuRef.current?.contains(target)) {\n        return;\n      }\n\n      setFormatOpen(false);\n      setHoveredFormat(null);\n    };\n\n    const handleKeyDown = (event: KeyboardEvent) => {\n      if (event.key === \"Escape\") {\n        event.stopPropagation();\n        setFormatOpen(false);\n        setHoveredFormat(null);\n      }\n    };\n\n    window.addEventListener(\"pointerdown\", handlePointerDown);\n    window.addEventListener(\"keydown\", handleKeyDown);\n\n    return () => {\n      window.removeEventListener(\"pointerdown\", handlePointerDown);\n      window.removeEventListener(\"keydown\", handleKeyDown);\n    };\n  }, [formatOpen]);\n\n  useEffect(() => {\n    if (!formatOpen) {\n      setHoveredFormat(null);\n    }\n  }, [formatOpen]);\n\n  const pickWithEyedropper = useCallback(async () => {\n    const browserWindow = window as Window & {\n      EyeDropper?: new () => { open: () => Promise<{ sRGBHex: string }> };\n    };\n\n    if (!browserWindow.EyeDropper) {\n      onEyedropperUnsupported?.();\n      return;\n    }\n\n    try {\n      const result = await new browserWindow.EyeDropper().open();\n      setFromHex(result.sRGBHex);\n    } catch {\n      // User cancelled the picker.\n    }\n  }, [onEyedropperUnsupported, setFromHex]);\n\n  const copyCurrentColor = useCallback(async () => {\n    const detail = buildChangeDetail(rgb, effectiveAlpha);\n    const copyValue =\n      format === \"RGB\"\n        ? detail.rgb\n        : format === \"HSL\"\n          ? detail.hsl\n          : format === \"OKLCH\"\n            ? detail.oklch\n            : detail.hex;\n\n    try {\n      await navigator.clipboard.writeText(copyValue);\n      setCopied(true);\n      if (copyTimeoutRef.current !== null) {\n        window.clearTimeout(copyTimeoutRef.current);\n      }\n      copyTimeoutRef.current = window.setTimeout(() => {\n        setCopied(false);\n        copyTimeoutRef.current = null;\n      }, 1500);\n    } catch {\n      // Clipboard unavailable.\n    }\n  }, [effectiveAlpha, format, rgb]);\n\n  const handleSvKeyDown = (event: ReactKeyboardEvent<HTMLDivElement>) => {\n    const step = event.shiftKey ? 5 : 1;\n    let nextSat = pickerSatRef.current;\n    let nextVal = pickerValRef.current;\n\n    switch (event.key) {\n      case \"ArrowLeft\":\n        event.preventDefault();\n        nextSat = Math.max(0, nextSat - step);\n        break;\n      case \"ArrowRight\":\n        event.preventDefault();\n        nextSat = Math.min(100, nextSat + step);\n        break;\n      case \"ArrowUp\":\n        event.preventDefault();\n        nextVal = Math.min(100, nextVal + step);\n        break;\n      case \"ArrowDown\":\n        event.preventDefault();\n        nextVal = Math.max(0, nextVal - step);\n        break;\n      default:\n        return;\n    }\n\n    applyPickerHsv(pickerHueRef.current, nextSat, nextVal, \"commit\");\n  };\n\n  const handleFormatTriggerKeyDown = (event: ReactKeyboardEvent) => {\n    if (event.key === \"Escape\" && formatOpen) {\n      event.preventDefault();\n      setFormatOpen(false);\n      return;\n    }\n\n    if (!formatOpen && (event.key === \"ArrowDown\" || event.key === \"Enter\")) {\n      event.preventDefault();\n      setFormatOpen(true);\n      return;\n    }\n\n    if (!formatOpen) {\n      return;\n    }\n\n    const currentIndex = FORMAT_OPTIONS.indexOf(format);\n    if (event.key === \"ArrowDown\") {\n      event.preventDefault();\n      setFormat(FORMAT_OPTIONS[(currentIndex + 1) % FORMAT_OPTIONS.length]);\n    }\n\n    if (event.key === \"ArrowUp\") {\n      event.preventDefault();\n      setFormat(\n        FORMAT_OPTIONS[\n          (currentIndex - 1 + FORMAT_OPTIONS.length) % FORMAT_OPTIONS.length\n        ]\n      );\n    }\n  };\n\n  const displayHex = getDisplayHex(rgb, effectiveAlpha, showAlpha);\n\n  return (\n    <fieldset\n      aria-label={ariaLabelledBy ? undefined : ariaLabel}\n      aria-labelledby={ariaLabelledBy}\n      className={cn(\n        componentThemeClassName,\n        controlCornerClassName,\n        fillWidth ? \"w-full min-w-0\" : colorPickerShellWidthClassName,\n        \"relative isolate m-0 min-w-0 overflow-visible border border-border bg-card p-3.5 shadow-[0_10px_28px_-18px_rgba(15,23,42,0.16)] dark:shadow-[0_12px_32px_-20px_rgba(0,0,0,0.35)]\",\n        disabled && \"pointer-events-none opacity-60\"\n      )}\n      data-slot=\"color-picker-panel\"\n      disabled={disabled}\n      id={id}\n      ref={panelRef}\n    >\n      <div\n        className={cn(\n          \"pointer-events-none absolute -inset-10 overflow-hidden opacity-[0.12] blur-2xl motion-reduce:opacity-0\",\n          controlCornerClassName\n        )}\n      >\n        <div\n          className=\"absolute top-[20%] left-[20%] h-32 w-32 animate-pulse rounded-full motion-reduce:animate-none\"\n          style={{ background: colorValue, transition: \"background 0.3s\" }}\n        />\n        <div\n          className=\"absolute right-[10%] bottom-[10%] h-24 w-24 animate-pulse rounded-full [animation-delay:1.2s] motion-reduce:animate-none\"\n          style={{\n            background: `#${hueHex}`,\n            transition: \"background 0.3s\",\n          }}\n        />\n      </div>\n\n      <div className=\"relative\">\n        {presets && presets.length > 0 ? (\n          <PresetSwatches\n            disabled={disabled}\n            onSelect={(color) => setFromHex(color)}\n            presets={presets}\n            swatchShape={swatchShape}\n          />\n        ) : null}\n\n        <div\n          aria-label=\"Saturation and brightness\"\n          className={cn(\n            \"relative h-40 w-full cursor-crosshair touch-none select-none overflow-hidden transition-[background-color] duration-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--color-ring)]\",\n            controlCornerClassName\n          )}\n          id={svFieldId}\n          onKeyDown={handleSvKeyDown}\n          onPointerDown={(event) => {\n            if (disabled) {\n              return;\n            }\n\n            dragRef.current = \"sv\";\n            isDraggingSvRef.current = true;\n            event.currentTarget.setPointerCapture(event.pointerId);\n            updateSV(event, \"none\");\n          }}\n          ref={svRef}\n          role=\"application\"\n          style={{ backgroundColor: `#${hueHex}` }}\n          tabIndex={disabled ? -1 : 0}\n        >\n          <div\n            className=\"absolute inset-0\"\n            style={{\n              background: \"linear-gradient(to right, #fff, transparent)\",\n            }}\n          />\n          <div\n            className=\"absolute inset-0\"\n            style={{ background: \"linear-gradient(to top, #000, transparent)\" }}\n          />\n          <div\n            aria-hidden\n            className=\"absolute h-3.5 w-3.5 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white shadow-sm\"\n            style={{\n              left: `${pickerSat}%`,\n              top: `${100 - pickerVal}%`,\n              backgroundColor: colorValue,\n            }}\n          />\n        </div>\n\n        <div\n          className=\"relative mt-3 h-2.5 rounded-full\"\n          style={{\n            background:\n              \"linear-gradient(to right, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00)\",\n          }}\n        >\n          <input\n            aria-label=\"Hue\"\n            aria-valuemax={360}\n            aria-valuemin={0}\n            aria-valuenow={Math.round(pickerHue)}\n            className=\"absolute inset-0 h-full w-full cursor-pointer opacity-0\"\n            disabled={disabled}\n            max={360}\n            min={0}\n            onChange={(event) => {\n              applyPickerHsv(\n                Number(event.target.value),\n                pickerSatRef.current,\n                pickerValRef.current,\n                \"none\"\n              );\n            }}\n            onPointerDown={(event) => {\n              dragRef.current = \"hue\";\n              isDraggingHueRef.current = true;\n              event.currentTarget.setPointerCapture(event.pointerId);\n            }}\n            step={1}\n            type=\"range\"\n            value={pickerHue}\n          />\n          <div\n            aria-hidden\n            className=\"pointer-events-none absolute top-1/2 h-3.5 w-3.5 -translate-x-1/2 -translate-y-1/2 rounded-full border border-black/10 bg-white shadow\"\n            style={{ left: `${(pickerHue / 360) * 100}%` }}\n          />\n        </div>\n\n        {showAlpha ? (\n          <div\n            className={cn(\n              \"relative mt-2.5 h-2.5 overflow-hidden rounded-full\",\n              alphaCheckerClassName\n            )}\n          >\n            <div\n              className=\"absolute inset-0\"\n              style={{\n                background: `linear-gradient(to right, transparent, ${colorValue})`,\n              }}\n            />\n            <input\n              aria-label=\"Alpha\"\n              aria-valuemax={100}\n              aria-valuemin={0}\n              aria-valuenow={alpha}\n              className=\"absolute inset-0 h-full w-full cursor-pointer opacity-0\"\n              disabled={disabled}\n              max={100}\n              min={0}\n              onChange={(event) => {\n                applyAlpha(Number(event.target.value), \"none\");\n              }}\n              onPointerDown={(event) => {\n                dragRef.current = \"alpha\";\n                isDraggingAlphaRef.current = true;\n                event.currentTarget.setPointerCapture(event.pointerId);\n              }}\n              step={1}\n              type=\"range\"\n              value={alpha}\n            />\n            <div\n              aria-hidden\n              className=\"pointer-events-none absolute top-1/2 h-3.5 w-3.5 -translate-x-1/2 -translate-y-1/2 rounded-full border border-black/10 bg-white shadow\"\n              style={{ left: `${alpha}%` }}\n            />\n          </div>\n        ) : null}\n\n        <div className=\"relative z-20 mt-4 flex items-center justify-between text-muted-foreground text-sm\">\n          <div className=\"relative z-30\" ref={formatMenuRef}>\n            <button\n              aria-controls={formatOpen ? formatMenuId : undefined}\n              aria-expanded={formatOpen}\n              aria-haspopup=\"listbox\"\n              aria-label=\"Color format\"\n              className={cn(\n                \"flex items-center gap-1 px-1 py-0.5 hover:bg-[color:var(--color-accent)] hover:text-foreground\",\n                controlCornerClassName,\n                formatTriggerTransitionClassName\n              )}\n              disabled={disabled}\n              id={formatTriggerId}\n              onClick={() => setFormatOpen((open) => !open)}\n              onKeyDown={handleFormatTriggerKeyDown}\n              type=\"button\"\n            >\n              {format}\n              <motion.span\n                animate={{ rotate: formatOpen ? 180 : 0 }}\n                transition={{ duration: 0.22, ease: FORMAT_MENU_SOFT_EASE }}\n              >\n                <ChevronDown className=\"h-3 w-3\" />\n              </motion.span>\n            </button>\n            <AnimatePresence>\n              {formatOpen ? (\n                <motion.div\n                  {...formatMenuMotion}\n                  className={cn(\n                    formatMenuPanelClassName,\n                    controlCornerClassName\n                  )}\n                  id={formatMenuId}\n                  role=\"listbox\"\n                >\n                  {FORMAT_OPTIONS.map((option, index) => {\n                    const isSelected = format === option;\n                    const isHovered = hoveredFormat === option;\n                    const showHighlight =\n                      isHovered || (hoveredFormat === null && isSelected);\n\n                    return (\n                      <motion.button\n                        {...getFormatItemMotion(index)}\n                        aria-selected={isSelected}\n                        className={cn(\n                          formatMenuItemClassName,\n                          controlCornerClassName,\n                          isSelected || isHovered\n                            ? \"text-foreground\"\n                            : \"text-muted-foreground\"\n                        )}\n                        key={option}\n                        onClick={() => {\n                          setFormat(option);\n                          setFormatOpen(false);\n                        }}\n                        onMouseEnter={() => setHoveredFormat(option)}\n                        onMouseLeave={() => setHoveredFormat(null)}\n                        role=\"option\"\n                        type=\"button\"\n                      >\n                        {showHighlight ? (\n                          <motion.span\n                            className={cn(\n                              formatMenuHighlightClassName,\n                              controlCornerInheritClassName\n                            )}\n                            layoutId=\"color-picker-format-highlight\"\n                            transition={formatMenuHighlightTransition}\n                          />\n                        ) : null}\n                        <span\n                          className={cn(\n                            \"relative z-10 block\",\n                            isSelected && \"font-medium\"\n                          )}\n                        >\n                          {option}\n                        </span>\n                      </motion.button>\n                    );\n                  })}\n                </motion.div>\n              ) : null}\n            </AnimatePresence>\n          </div>\n\n          <div className=\"flex items-center gap-2\">\n            {showCopy ? (\n              <button\n                aria-label={copied ? \"Copied\" : \"Copy color value\"}\n                className=\"transition-colors hover:text-foreground\"\n                disabled={disabled}\n                onClick={copyCurrentColor}\n                type=\"button\"\n              >\n                {copied ? (\n                  <Check className=\"h-4 w-4\" />\n                ) : (\n                  <Copy className=\"h-4 w-4\" />\n                )}\n              </button>\n            ) : null}\n            {showEyedropper ? (\n              <button\n                aria-label=\"Pick color from screen\"\n                className=\"transition-colors hover:text-foreground\"\n                disabled={disabled}\n                onClick={pickWithEyedropper}\n                title=\"Pick color from screen\"\n                type=\"button\"\n              >\n                <Pipette className=\"h-4 w-4\" />\n              </button>\n            ) : null}\n          </div>\n        </div>\n\n        <div className=\"relative z-10 mt-2 flex items-start gap-2 text-foreground\">\n          {format === \"HEX\" ? (\n            <>\n              <div className=\"min-w-0 flex-1\">\n                <div className=\"flex items-center gap-2\">\n                  <ColorSwatch\n                    alpha={effectiveAlpha}\n                    className=\"h-5 w-5 shrink-0\"\n                    color={colorValue}\n                    shape={swatchShape}\n                  />\n                  <div className=\"flex min-w-0 flex-1 items-center gap-0.5\">\n                    <span className=\"font-light text-base text-muted-foreground\">\n                      #\n                    </span>\n                    <ColorPickerTextInput\n                      aria-describedby={\n                        hexInvalid ? `${svFieldId}-hex-error` : undefined\n                      }\n                      aria-invalid={hexInvalid || undefined}\n                      aria-label=\"Hex color\"\n                      className={cn(\n                        \"w-full min-w-0 bg-transparent font-light text-base tracking-wide outline-none\",\n                        hexInvalid && \"text-destructive\"\n                      )}\n                      disabled={disabled}\n                      onBlur={() => {\n                        isEditingRef.current = false;\n                        setHexFocused(false);\n                        const committed = commitHexInput(hexInput, \"commit\");\n                        setHexInvalid(!committed && hexInput.trim().length > 0);\n                      }}\n                      onFocus={() => {\n                        isEditingRef.current = true;\n                        setHexInput(displayHex);\n                        setHexFocused(true);\n                        setHexInvalid(false);\n                      }}\n                      onKeyDown={(event) => {\n                        if (event.key === \"Enter\") {\n                          event.currentTarget.blur();\n                        }\n                      }}\n                      onValueChange={(nextValue) => {\n                        const maxLength = showAlpha ? 8 : 6;\n                        const sanitized = nextValue\n                          .replace(HEX_INPUT_SANITIZE_PATTERN, \"\")\n                          .slice(0, maxLength)\n                          .toUpperCase();\n                        setHexInput(sanitized);\n                        setHexInvalid(false);\n\n                        if (\n                          sanitized.length === 6 ||\n                          (showAlpha && sanitized.length === 8)\n                        ) {\n                          commitHexInput(sanitized, \"change\");\n                        }\n                      }}\n                      spellCheck={false}\n                      value={hexFocused ? hexInput : displayHex}\n                    />\n                  </div>\n                </div>\n                {hexInvalid ? (\n                  <p\n                    className=\"mt-1 text-destructive text-xs\"\n                    id={`${svFieldId}-hex-error`}\n                  >\n                    Enter a valid hex color.\n                  </p>\n                ) : null}\n              </div>\n              {showAlpha ? (\n                <ChannelInput\n                  disabled={disabled}\n                  label=\"Alpha\"\n                  max={100}\n                  min={0}\n                  onChange={(nextAlpha) => applyAlpha(nextAlpha, \"commit\")}\n                  onEditEnd={() => {\n                    isEditingRef.current = false;\n                  }}\n                  onEditStart={() => {\n                    isEditingRef.current = true;\n                  }}\n                  suffix=\"%\"\n                  value={alpha}\n                />\n              ) : null}\n            </>\n          ) : null}\n          {format === \"RGB\" ? (\n            <>\n              <ChannelInput\n                disabled={disabled}\n                label=\"Red\"\n                max={255}\n                min={0}\n                onChange={(nextRed) => applyRgb({ r: nextRed, g, b }, \"commit\")}\n                onEditEnd={() => {\n                  isEditingRef.current = false;\n                }}\n                onEditStart={() => {\n                  isEditingRef.current = true;\n                }}\n                value={r}\n              />\n              <ChannelInput\n                disabled={disabled}\n                label=\"Green\"\n                max={255}\n                min={0}\n                onChange={(nextGreen) =>\n                  applyRgb({ r, g: nextGreen, b }, \"commit\")\n                }\n                onEditEnd={() => {\n                  isEditingRef.current = false;\n                }}\n                onEditStart={() => {\n                  isEditingRef.current = true;\n                }}\n                value={g}\n              />\n              <ChannelInput\n                disabled={disabled}\n                label=\"Blue\"\n                max={255}\n                min={0}\n                onChange={(nextBlue) =>\n                  applyRgb({ r, g, b: nextBlue }, \"commit\")\n                }\n                onEditEnd={() => {\n                  isEditingRef.current = false;\n                }}\n                onEditStart={() => {\n                  isEditingRef.current = true;\n                }}\n                value={b}\n              />\n              {showAlpha ? (\n                <ChannelInput\n                  disabled={disabled}\n                  label=\"Alpha\"\n                  max={100}\n                  min={0}\n                  onChange={(nextAlpha) => applyAlpha(nextAlpha, \"commit\")}\n                  onEditEnd={() => {\n                    isEditingRef.current = false;\n                  }}\n                  onEditStart={() => {\n                    isEditingRef.current = true;\n                  }}\n                  suffix=\"%\"\n                  value={alpha}\n                />\n              ) : null}\n            </>\n          ) : null}\n          {format === \"HSL\" ? (\n            <>\n              <ChannelInput\n                disabled={disabled}\n                label=\"Hue\"\n                max={360}\n                min={0}\n                onChange={(nextHue) => setFromHsl(nextHue, hsl.s, hsl.l)}\n                onEditEnd={() => {\n                  isEditingRef.current = false;\n                }}\n                onEditStart={() => {\n                  isEditingRef.current = true;\n                }}\n                value={hsl.h}\n              />\n              <ChannelInput\n                disabled={disabled}\n                label=\"Saturation\"\n                max={100}\n                min={0}\n                onChange={(nextSaturation) =>\n                  setFromHsl(hsl.h, nextSaturation, hsl.l)\n                }\n                onEditEnd={() => {\n                  isEditingRef.current = false;\n                }}\n                onEditStart={() => {\n                  isEditingRef.current = true;\n                }}\n                value={hsl.s}\n              />\n              <ChannelInput\n                disabled={disabled}\n                label=\"Lightness\"\n                max={100}\n                min={0}\n                onChange={(nextLightness) =>\n                  setFromHsl(hsl.h, hsl.s, nextLightness)\n                }\n                onEditEnd={() => {\n                  isEditingRef.current = false;\n                }}\n                onEditStart={() => {\n                  isEditingRef.current = true;\n                }}\n                value={hsl.l}\n              />\n              {showAlpha ? (\n                <ChannelInput\n                  disabled={disabled}\n                  label=\"Alpha\"\n                  max={100}\n                  min={0}\n                  onChange={(nextAlpha) => applyAlpha(nextAlpha, \"commit\")}\n                  onEditEnd={() => {\n                    isEditingRef.current = false;\n                  }}\n                  onEditStart={() => {\n                    isEditingRef.current = true;\n                  }}\n                  suffix=\"%\"\n                  value={alpha}\n                />\n              ) : null}\n            </>\n          ) : null}\n          {format === \"OKLCH\" ? (\n            <>\n              <ChannelInput\n                disabled={disabled}\n                label=\"Lightness\"\n                max={1}\n                min={0}\n                onChange={(nextLightness) =>\n                  setFromOklch(nextLightness, oklch.c, oklch.h)\n                }\n                onEditEnd={() => {\n                  isEditingRef.current = false;\n                }}\n                onEditStart={() => {\n                  isEditingRef.current = true;\n                }}\n                step={0.01}\n                value={Math.round(oklch.l * 100) / 100}\n              />\n              <ChannelInput\n                disabled={disabled}\n                label=\"Chroma\"\n                max={oklchMaxChroma}\n                min={0}\n                onChange={(nextChroma) =>\n                  setFromOklch(oklch.l, nextChroma, oklch.h)\n                }\n                onEditEnd={() => {\n                  isEditingRef.current = false;\n                }}\n                onEditStart={() => {\n                  isEditingRef.current = true;\n                }}\n                step={0.001}\n                value={Math.round(oklch.c * 1000) / 1000}\n              />\n              <ChannelInput\n                disabled={disabled}\n                label=\"Hue\"\n                max={360}\n                min={0}\n                onChange={(nextHue) => setFromOklch(oklch.l, oklch.c, nextHue)}\n                onEditEnd={() => {\n                  isEditingRef.current = false;\n                }}\n                onEditStart={() => {\n                  isEditingRef.current = true;\n                }}\n                value={Math.round(oklch.h)}\n              />\n              {showAlpha ? (\n                <ChannelInput\n                  disabled={disabled}\n                  label=\"Alpha\"\n                  max={100}\n                  min={0}\n                  onChange={(nextAlpha) => applyAlpha(nextAlpha, \"commit\")}\n                  onEditEnd={() => {\n                    isEditingRef.current = false;\n                  }}\n                  onEditStart={() => {\n                    isEditingRef.current = true;\n                  }}\n                  suffix=\"%\"\n                  value={alpha}\n                />\n              ) : null}\n            </>\n          ) : null}\n        </div>\n      </div>\n    </fieldset>\n  );\n}\n\nfunction getPopoverPanelOffset(placement: \"top\" | \"bottom\") {\n  return placement === \"bottom\" ? -10 : 10;\n}\n\nfunction getPopoverPanelMotion(\n  placement: \"top\" | \"bottom\",\n  reducedMotion: boolean\n) {\n  if (reducedMotion) {\n    return {\n      animate: { opacity: 1 },\n      exit: { opacity: 0, transition: { duration: 0.12 } },\n      initial: { opacity: 0 },\n      transition: { duration: 0.12 },\n    };\n  }\n\n  const enterOffset = getPopoverPanelOffset(placement);\n  const exitOffset = placement === \"bottom\" ? -5 : 5;\n\n  return {\n    animate: { opacity: 1, scale: 1, y: 0 },\n    exit: {\n      opacity: 0,\n      scale: 0.97,\n      y: exitOffset,\n      transition: {\n        opacity: { duration: 0.2, ease: EASE_IN_OUT },\n        scale: { duration: 0.2, ease: EASE_IN_OUT },\n        y: { duration: 0.2, ease: EASE_IN_OUT },\n      },\n    },\n    initial: { opacity: 0, scale: 0.94, y: enterOffset },\n    transition: {\n      opacity: { duration: 0.26, ease: EASE_OUT },\n      scale: SPRING_PANEL,\n      y: SPRING_PANEL,\n    },\n  };\n}\n\nconst POPOVER_VIEWPORT_PADDING = 12;\nconst POPOVER_GAP = 8;\n\nfunction getColorPickerPopoverPosition({\n  align,\n  anchorRect,\n  matchAnchorWidth,\n  panelRect,\n}: {\n  align: \"center\" | \"start\";\n  anchorRect: DOMRect;\n  matchAnchorWidth: boolean;\n  panelRect: DOMRect;\n}) {\n  const panelWidth = matchAnchorWidth ? anchorRect.width : panelRect.width;\n  let placement: \"bottom\" | \"top\" = \"bottom\";\n  let top = anchorRect.bottom + POPOVER_GAP;\n  let left =\n    align === \"center\"\n      ? anchorRect.left + anchorRect.width / 2 - panelWidth / 2\n      : anchorRect.left;\n\n  if (left + panelWidth > window.innerWidth - POPOVER_VIEWPORT_PADDING) {\n    left = window.innerWidth - panelWidth - POPOVER_VIEWPORT_PADDING;\n  }\n\n  if (left < POPOVER_VIEWPORT_PADDING) {\n    left = POPOVER_VIEWPORT_PADDING;\n  }\n\n  if (top + panelRect.height > window.innerHeight - POPOVER_VIEWPORT_PADDING) {\n    top = anchorRect.top - panelRect.height - POPOVER_GAP;\n    placement = \"top\";\n  }\n\n  return {\n    left: Math.round(left),\n    placement,\n    top: Math.round(top),\n    width: matchAnchorWidth ? Math.round(anchorRect.width) : undefined,\n  };\n}\n\nfunction ColorPickerPopoverLayer({\n  align = \"start\",\n  anchorRef,\n  children,\n  matchAnchorWidth = false,\n  onClose,\n  open,\n  panelId,\n  triggerId,\n}: {\n  align?: \"center\" | \"start\";\n  anchorRef: RefObject<HTMLElement | null>;\n  children: ReactNode;\n  matchAnchorWidth?: boolean;\n  onClose: () => void;\n  open: boolean;\n  panelId: string;\n  triggerId: string;\n}) {\n  const floatingRef = useRef<HTMLDivElement>(null);\n  const reducedMotion = useReducedMotion();\n  const [position, setPosition] = useState({\n    left: 0,\n    placement: \"bottom\" as \"bottom\" | \"top\",\n    top: 0,\n    width: undefined as number | undefined,\n  });\n  const [isPositioned, setIsPositioned] = useState(false);\n  const [mounted, setMounted] = useState(false);\n  const panelMotion = getPopoverPanelMotion(\n    position.placement,\n    reducedMotion ?? false\n  );\n\n  const handleClose = useCallback(() => {\n    onClose();\n    anchorRef.current?.focus({ preventScroll: true });\n  }, [anchorRef, onClose]);\n\n  useEffect(() => {\n    setMounted(true);\n  }, []);\n\n  useLayoutEffect(() => {\n    if (!open) {\n      setIsPositioned(false);\n      return;\n    }\n\n    const updatePosition = () => {\n      const anchor = anchorRef.current;\n      const panel = floatingRef.current;\n\n      if (!(anchor && panel)) {\n        return;\n      }\n\n      const anchorRect = anchor.getBoundingClientRect();\n      const panelRect = panel.getBoundingClientRect();\n\n      setPosition(\n        getColorPickerPopoverPosition({\n          align,\n          anchorRect,\n          matchAnchorWidth,\n          panelRect,\n        })\n      );\n      setIsPositioned(true);\n    };\n\n    updatePosition();\n    const raf = window.requestAnimationFrame(updatePosition);\n    window.addEventListener(\"resize\", updatePosition);\n    window.addEventListener(\"scroll\", updatePosition, true);\n\n    return () => {\n      window.cancelAnimationFrame(raf);\n      window.removeEventListener(\"resize\", updatePosition);\n      window.removeEventListener(\"scroll\", updatePosition, true);\n    };\n  }, [align, anchorRef, matchAnchorWidth, open]);\n\n  useEffect(() => {\n    if (!(open && isPositioned)) {\n      return;\n    }\n\n    const panel = floatingRef.current;\n    const raf = window.requestAnimationFrame(() => {\n      const focusTarget = panel?.querySelector<HTMLElement>(\n        '[role=\"application\"][tabindex=\"0\"], button:not([disabled]), input:not([disabled])'\n      );\n\n      focusTarget?.focus({ preventScroll: true });\n    });\n\n    return () => {\n      window.cancelAnimationFrame(raf);\n    };\n  }, [isPositioned, open]);\n\n  useEffect(() => {\n    if (!open) {\n      return;\n    }\n\n    const handlePointerDown = (event: PointerEvent) => {\n      const target = event.target as Node;\n\n      if (\n        anchorRef.current?.contains(target) ||\n        floatingRef.current?.contains(target)\n      ) {\n        return;\n      }\n\n      handleClose();\n    };\n\n    const handleKeyDown = (event: KeyboardEvent) => {\n      if (event.key === \"Escape\") {\n        handleClose();\n      }\n    };\n\n    window.addEventListener(\"pointerdown\", handlePointerDown);\n    window.addEventListener(\"keydown\", handleKeyDown);\n\n    return () => {\n      window.removeEventListener(\"pointerdown\", handlePointerDown);\n      window.removeEventListener(\"keydown\", handleKeyDown);\n    };\n  }, [anchorRef, handleClose, open]);\n\n  if (!mounted) {\n    return null;\n  }\n\n  return createPortal(\n    <AnimatePresence>\n      {open ? (\n        <motion.div\n          animate={panelMotion.animate}\n          aria-labelledby={triggerId}\n          aria-modal=\"true\"\n          className=\"fixed z-[100]\"\n          exit={panelMotion.exit}\n          id={panelId}\n          initial={panelMotion.initial}\n          ref={floatingRef}\n          role=\"dialog\"\n          style={{\n            left: position.left,\n            top: position.top,\n            transformOrigin:\n              position.placement === \"top\" ? \"bottom center\" : \"top center\",\n            visibility: isPositioned ? \"visible\" : \"hidden\",\n            width: position.width,\n          }}\n          transition={panelMotion.transition}\n        >\n          {children}\n        </motion.div>\n      ) : null}\n    </AnimatePresence>,\n    document.body\n  );\n}\n\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: picker shell coordinates SV/hue/alpha drags, format menu, and variant layouts in one place.\nexport const ColorPicker = forwardRef(function ColorPicker(\n  {\n    \"aria-label\": ariaLabel = \"Color picker\",\n    \"aria-labelledby\": ariaLabelledBy,\n    className,\n    defaultAlpha = 100,\n    defaultFormat = \"HEX\",\n    defaultOpen = false,\n    defaultValue = \"#3B82F6\",\n    disabled = false,\n    id: idProp,\n    name,\n    onChange,\n    onEyedropperUnsupported,\n    onOpenChange,\n    onValueCommit,\n    open: openProp,\n    placeholder = \"Pick a color\",\n    presets,\n    showAlpha = true,\n    showCopy = false,\n    showEyedropper = true,\n    swatchShape = \"default\",\n    value,\n    variant = \"inline\",\n  }: ColorPickerProps,\n  ref: ForwardedRef<HTMLDivElement>\n) {\n  const generatedId = useId();\n  const rootId = idProp ?? generatedId;\n  const panelId = `${rootId}-panel`;\n  const triggerId = `${rootId}-trigger`;\n  const formatMenuId = `${rootId}-format-menu`;\n  const formatTriggerId = `${rootId}-format-trigger`;\n  const svFieldId = `${rootId}-sv-field`;\n  const rootRef = useRef<HTMLDivElement>(null);\n  const triggerRef = useRef<HTMLButtonElement>(null);\n  const [open, setOpen] = useControllableOpen(\n    openProp,\n    defaultOpen,\n    onOpenChange\n  );\n\n  useImperativeHandle(ref, () => rootRef.current as HTMLDivElement);\n\n  const controller = useColorPickerController({\n    defaultAlpha,\n    defaultFormat,\n    defaultValue,\n    onChange,\n    onValueCommit,\n    showAlpha,\n    value,\n  });\n\n  const hiddenInput = name ? (\n    <InputPrimitive\n      name={name}\n      readOnly\n      render={(inputProps) => (\n        <input {...inputProps} tabIndex={-1} type=\"hidden\" />\n      )}\n      tabIndex={-1}\n      value={controller.colorValue}\n    />\n  ) : null;\n\n  if (variant === \"popover\" || variant === \"swatch\") {\n    const detail = buildChangeDetail(controller.rgb, controller.effectiveAlpha);\n    const activeFormat = controller.format;\n    const triggerLabel =\n      activeFormat === \"HEX\"\n        ? detail.hex\n        : activeFormat === \"RGB\"\n          ? detail.rgb\n          : activeFormat === \"HSL\"\n            ? detail.hsl\n            : detail.oklch;\n    const swatchTriggerLabel = controller.colorValue\n      ? `${ariaLabel}: ${detail.hex}`\n      : ariaLabel;\n\n    return (\n      <div\n        className={cn(\n          \"relative inline-flex\",\n          variant === \"popover\" && colorPickerShellWidthClassName,\n          className\n        )}\n        data-slot=\"color-picker\"\n        ref={rootRef}\n      >\n        {hiddenInput}\n        {variant === \"popover\" ? (\n          <button\n            aria-controls={open ? panelId : undefined}\n            aria-expanded={open}\n            aria-haspopup=\"dialog\"\n            aria-label={ariaLabel}\n            aria-labelledby={ariaLabelledBy}\n            className={popoverTriggerClassName}\n            disabled={disabled}\n            id={triggerId}\n            onClick={() => {\n              const nextOpen = !open;\n              setOpen(nextOpen);\n\n              if (!nextOpen) {\n                triggerRef.current?.focus({ preventScroll: true });\n              }\n            }}\n            ref={triggerRef}\n            type=\"button\"\n          >\n            <ColorSwatch\n              alpha={controller.effectiveAlpha}\n              className=\"h-5 w-5 shrink-0\"\n              color={controller.colorValue}\n            />\n            <span className=\"min-w-0 flex-1 truncate font-medium tracking-tight\">\n              {controller.colorValue ? triggerLabel : placeholder}\n            </span>\n            <ChevronDown\n              className={cn(\n                \"h-4 w-4 shrink-0 text-muted-foreground transition-transform\",\n                open && \"rotate-180\"\n              )}\n            />\n          </button>\n        ) : (\n          <button\n            aria-controls={open ? panelId : undefined}\n            aria-expanded={open}\n            aria-haspopup=\"dialog\"\n            aria-label={swatchTriggerLabel}\n            aria-labelledby={ariaLabelledBy}\n            className={cn(\n              swatchTriggerClassName,\n              swatchShape === \"circle\" && \"rounded-full\",\n              disabled && \"pointer-events-none opacity-60\"\n            )}\n            disabled={disabled}\n            id={triggerId}\n            onClick={() => {\n              const nextOpen = !open;\n              setOpen(nextOpen);\n\n              if (!nextOpen) {\n                triggerRef.current?.focus({ preventScroll: true });\n              }\n            }}\n            ref={triggerRef}\n            type=\"button\"\n          >\n            <ColorSwatch\n              alpha={controller.effectiveAlpha}\n              className=\"h-8 w-8 shadow-sm\"\n              color={controller.colorValue}\n              shape={swatchShape}\n            />\n          </button>\n        )}\n\n        <ColorPickerPopoverLayer\n          align={variant === \"popover\" ? \"start\" : \"center\"}\n          anchorRef={triggerRef}\n          matchAnchorWidth={variant === \"popover\"}\n          onClose={() => setOpen(false)}\n          open={open}\n          panelId={panelId}\n          triggerId={triggerId}\n        >\n          <ColorPickerPanel\n            ariaLabel={ariaLabel}\n            ariaLabelledBy={ariaLabelledBy}\n            controller={controller}\n            disabled={disabled}\n            fillWidth={variant === \"popover\"}\n            formatMenuId={formatMenuId}\n            formatTriggerId={formatTriggerId}\n            id={panelId}\n            onEyedropperUnsupported={onEyedropperUnsupported}\n            presets={presets}\n            showAlpha={showAlpha}\n            showCopy={showCopy}\n            showEyedropper={showEyedropper}\n            svFieldId={svFieldId}\n            swatchShape={swatchShape}\n          />\n        </ColorPickerPopoverLayer>\n      </div>\n    );\n  }\n\n  return (\n    // biome-ignore lint/a11y/useSemanticElements: fieldset styling would fight inline layout; panel is already a fieldset.\n    <div\n      aria-disabled={disabled || undefined}\n      aria-label={ariaLabel}\n      aria-labelledby={ariaLabelledBy}\n      className={cn(className)}\n      data-slot=\"color-picker\"\n      id={rootId}\n      ref={rootRef}\n      role=\"group\"\n    >\n      {hiddenInput}\n      <ColorPickerPanel\n        ariaLabel={ariaLabel}\n        ariaLabelledBy={ariaLabelledBy}\n        controller={controller}\n        disabled={disabled}\n        formatMenuId={formatMenuId}\n        formatTriggerId={formatTriggerId}\n        onEyedropperUnsupported={onEyedropperUnsupported}\n        presets={presets}\n        showAlpha={showAlpha}\n        showCopy={showCopy}\n        showEyedropper={showEyedropper}\n        svFieldId={svFieldId}\n        swatchShape={swatchShape}\n      />\n    </div>\n  );\n});\n\nColorPicker.displayName = \"ColorPicker\";\n\nfunction ColorPickerTextInput({\n  \"aria-describedby\": ariaDescribedBy,\n  \"aria-invalid\": ariaInvalid,\n  \"aria-label\": ariaLabel,\n  className,\n  disabled,\n  inputMode,\n  onBlur,\n  onFocus,\n  onKeyDown,\n  onValueChange,\n  spellCheck,\n  value,\n}: {\n  \"aria-describedby\"?: string;\n  \"aria-invalid\"?: boolean;\n  \"aria-label\": string;\n  className?: string;\n  disabled?: boolean;\n  inputMode?: HTMLAttributes<HTMLInputElement>[\"inputMode\"];\n  onBlur?: () => void;\n  onFocus?: () => void;\n  onKeyDown?: (event: ReactKeyboardEvent<HTMLInputElement>) => void;\n  onValueChange: (value: string) => void;\n  spellCheck?: boolean;\n  value: string;\n}) {\n  return (\n    <InputPrimitive\n      aria-describedby={ariaDescribedBy}\n      aria-invalid={ariaInvalid}\n      aria-label={ariaLabel}\n      disabled={disabled}\n      inputMode={inputMode}\n      onBlur={onBlur}\n      onFocus={onFocus}\n      onKeyDown={onKeyDown}\n      onValueChange={onValueChange}\n      render={(inputProps) => {\n        const {\n          className: primitiveClassName,\n          ref: _ref,\n          ...resolvedInputProps\n        } = inputProps;\n\n        return (\n          <input\n            {...resolvedInputProps}\n            className={cn(className, primitiveClassName)}\n            data-slot=\"color-picker-input\"\n            spellCheck={spellCheck}\n            type=\"text\"\n          />\n        );\n      }}\n      value={value}\n    />\n  );\n}\n\nfunction ChannelInput({\n  value,\n  min,\n  max,\n  step: _step = 1,\n  suffix,\n  label,\n  disabled,\n  onChange,\n  onEditEnd,\n  onEditStart,\n}: {\n  value: number;\n  min: number;\n  max: number;\n  step?: number;\n  suffix?: string;\n  label: string;\n  disabled?: boolean;\n  onChange: (value: number) => void;\n  onEditEnd?: () => void;\n  onEditStart?: () => void;\n}) {\n  const [local, setLocal] = useState(String(value));\n  const [focused, setFocused] = useState(false);\n  const displayValue = focused ? local : String(value);\n\n  useEffect(() => {\n    if (!focused) {\n      setLocal(String(value));\n    }\n  }, [focused, value]);\n\n  const commit = () => {\n    const parsed = Number(local);\n\n    if (Number.isNaN(parsed) || local.trim() === \"\") {\n      setLocal(String(value));\n      return;\n    }\n\n    onChange(Math.max(min, Math.min(max, parsed)));\n  };\n\n  return (\n    <div className=\"flex min-w-0 flex-1 flex-col items-center gap-0.5\">\n      <div className=\"flex w-full min-w-0 items-baseline justify-center gap-0.5\">\n        <ColorPickerTextInput\n          aria-label={label}\n          className=\"w-full min-w-0 bg-transparent text-center font-light text-base tabular-nums outline-none disabled:cursor-not-allowed disabled:opacity-50\"\n          disabled={disabled}\n          inputMode=\"decimal\"\n          onBlur={() => {\n            setFocused(false);\n            onEditEnd?.();\n            commit();\n          }}\n          onFocus={() => {\n            onEditStart?.();\n            setLocal(String(value));\n            setFocused(true);\n          }}\n          onKeyDown={(event) => {\n            if (event.key === \"Escape\") {\n              setLocal(String(value));\n              setFocused(false);\n              onEditEnd?.();\n              event.currentTarget.blur();\n              return;\n            }\n\n            if (event.key === \"Enter\") {\n              event.currentTarget.blur();\n            }\n          }}\n          onValueChange={setLocal}\n          value={displayValue}\n        />\n        {suffix ? (\n          <span className=\"text-muted-foreground text-sm\">{suffix}</span>\n        ) : null}\n      </div>\n      <span className=\"text-[10px] text-muted-foreground uppercase tracking-wide\">\n        {label}\n      </span>\n    </div>\n  );\n}\n\nexport { ColorPicker as FluidColorPicker };\nexport default ColorPicker;\n",
      "type": "registry:ui"
    }
  ],
  "title": "Color Picker",
  "description": "HSV panel with saturation field, hue/alpha sliders, multi-format readouts, hex input, and EyeDropper."
}
