{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "color-picker",
  "title": "Color Picker",
  "description": "A controlled or uncontrolled color picker with optional RGB, Hex, RGBA, RYB, CMYK, HSL, HSV, and preset controls.",
  "dependencies": [
    "@base-ui/react",
    "lucide-react"
  ],
  "files": [
    {
      "path": "registry/rivetui/components/color-picker/color-picker.tsx",
      "content": "\"use client\";\n\nimport { Popover as PopoverPrimitive } from \"@base-ui/react/popover\";\nimport { Check, Copy, TriangleAlert } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst DEFAULT_COLOR = \"#18181B\";\nconst DEFAULT_DISPLAY_FORMAT: ColorPickerInput = \"hex\";\nconst FALLBACK_COLOR: Color = { r: 24, g: 24, b: 27, a: 1 };\nconst COLOR_DRAG_UPDATE_INTERVAL = 12;\nconst COPY_FEEDBACK_DURATION = 1800;\nconst COPY_FEEDBACK_FADE_DURATION = 100;\nconst TRANSPARENCY_GRID_STYLE: React.CSSProperties = {\n  backgroundColor: \"var(--background)\",\n  backgroundImage:\n    \"linear-gradient(45deg, var(--border) 25%, transparent 25%), linear-gradient(-45deg, var(--border) 25%, transparent 25%), linear-gradient(45deg, transparent 75%, var(--border) 75%), linear-gradient(-45deg, transparent 75%, var(--border) 75%)\",\n  backgroundPosition: \"0 0, 0 3px, 3px -3px, -3px 0\",\n  backgroundSize: \"6px 6px\",\n};\n\ntype ColorPickerInput = \"hex\" | \"rgb\" | \"rgba\" | \"ryb\" | \"cmyk\" | \"hsl\" | \"hsv\";\ntype NumericColorPickerInput = Exclude<ColorPickerInput, \"hex\">;\ntype ChannelName = \"r\" | \"g\" | \"b\" | \"a\" | \"y\" | \"c\" | \"m\" | \"k\" | \"h\" | \"s\" | \"l\" | \"v\";\ntype ChannelDrafts = Partial<Record<ChannelName, string>>;\ntype ChannelValues = Partial<Record<ChannelName, number>>;\ntype ChannelDraftState = {\n  input: ColorPickerInput | undefined;\n  values: ChannelDrafts;\n};\ntype CopyStatus = \"idle\" | \"success\" | \"error\";\n\ntype Color = {\n  r: number;\n  g: number;\n  b: number;\n  a: number;\n};\n\ntype ChannelDefinition = {\n  name: ChannelName;\n  minimum: number;\n  maximum: number;\n  step: number;\n};\n\ntype InputDefinition = {\n  channels: readonly ChannelDefinition[];\n  hint: string;\n  label: string;\n};\n\nconst RGB_CHANNELS = [\n  { name: \"r\", minimum: 0, maximum: 255, step: 1 },\n  { name: \"g\", minimum: 0, maximum: 255, step: 1 },\n  { name: \"b\", minimum: 0, maximum: 255, step: 1 },\n] as const satisfies readonly ChannelDefinition[];\n\nconst INPUT_DEFINITIONS: Record<NumericColorPickerInput, InputDefinition> = {\n  rgb: {\n    label: \"RGB channels\",\n    hint: \"Red, green, and blue use 0–255.\",\n    channels: RGB_CHANNELS,\n  },\n  rgba: {\n    label: \"RGBA channels\",\n    hint: \"RGB uses 0–255; alpha uses 0–1.\",\n    channels: [...RGB_CHANNELS, { name: \"a\", minimum: 0, maximum: 1, step: 0.01 }],\n  },\n  ryb: {\n    label: \"RYB channels\",\n    hint: \"Approximate paint-model channels use 0–255.\",\n    channels: [\n      { name: \"r\", minimum: 0, maximum: 255, step: 1 },\n      { name: \"y\", minimum: 0, maximum: 255, step: 1 },\n      { name: \"b\", minimum: 0, maximum: 255, step: 1 },\n    ],\n  },\n  cmyk: {\n    label: \"CMYK channels\",\n    hint: \"Cyan, magenta, yellow, and key use 0–100.\",\n    channels: [\n      { name: \"c\", minimum: 0, maximum: 100, step: 1 },\n      { name: \"m\", minimum: 0, maximum: 100, step: 1 },\n      { name: \"y\", minimum: 0, maximum: 100, step: 1 },\n      { name: \"k\", minimum: 0, maximum: 100, step: 1 },\n    ],\n  },\n  hsl: {\n    label: \"HSL channels\",\n    hint: \"Hue uses 0–360; saturation and lightness use 0–100.\",\n    channels: [\n      { name: \"h\", minimum: 0, maximum: 360, step: 1 },\n      { name: \"s\", minimum: 0, maximum: 100, step: 1 },\n      { name: \"l\", minimum: 0, maximum: 100, step: 1 },\n    ],\n  },\n  hsv: {\n    label: \"HSV channels\",\n    hint: \"Hue uses 0–360; saturation and value use 0–100.\",\n    channels: [\n      { name: \"h\", minimum: 0, maximum: 360, step: 1 },\n      { name: \"s\", minimum: 0, maximum: 100, step: 1 },\n      { name: \"v\", minimum: 0, maximum: 100, step: 1 },\n    ],\n  },\n};\n\ntype ColorPickerProps = Omit<\n  React.ComponentProps<\"button\">,\n  \"defaultValue\" | \"name\" | \"onChange\" | \"value\"\n> & {\n  defaultOpen?: boolean;\n  defaultValue?: string;\n  displayFormat?: ColorPickerInput;\n  inputs?: ColorPickerInput | readonly ColorPickerInput[];\n  name?: string;\n  onOpenChange?: PopoverPrimitive.Root.Props[\"onOpenChange\"];\n  onValueChange?: (value: string) => void;\n  open?: boolean;\n  presets?: readonly string[];\n  value?: string;\n};\n\nfunction ColorPicker({\n  children,\n  className,\n  defaultOpen,\n  defaultValue = DEFAULT_COLOR,\n  disabled,\n  displayFormat = DEFAULT_DISPLAY_FORMAT,\n  inputs,\n  name,\n  onOpenChange,\n  onValueChange,\n  open,\n  presets,\n  value,\n  ...props\n}: ColorPickerProps) {\n  const initialColor = parseColor(defaultValue) ?? FALLBACK_COLOR;\n  const [uncontrolledColor, setUncontrolledColor] = React.useState(initialColor);\n  const selectedColor =\n    value === undefined ? uncontrolledColor : (parseColor(value) ?? initialColor);\n  const selectedValue = serializeColor(selectedColor);\n  const displayedValue = formatDisplayValue(selectedColor, displayFormat);\n  const normalizedPresets = (presets ?? [])\n    .map(parseColor)\n    .filter((preset): preset is Color => preset !== null)\n    .filter(\n      (preset, index, colors) =>\n        colors.findIndex((color) => serializeColor(color) === serializeColor(preset)) === index,\n    );\n  const enabledInputs = normalizeInputs(inputs, normalizedPresets.length > 0);\n  const [selectedInput, setSelectedInput] = React.useState<ColorPickerInput | undefined>(\n    enabledInputs[0],\n  );\n  const activeInput =\n    selectedInput && enabledInputs.includes(selectedInput) ? selectedInput : enabledInputs[0];\n  const [hexDraft, setHexDraft] = React.useState(selectedValue);\n  const [channelDraftState, setChannelDraftState] = React.useState<ChannelDraftState>(() => ({\n    input: activeInput,\n    values: getChannelDrafts(selectedColor, activeInput),\n  }));\n  const channelDrafts =\n    channelDraftState.input === activeInput\n      ? channelDraftState.values\n      : getChannelDrafts(selectedColor, activeInput);\n  const colorInputId = React.useId();\n  const hexInputId = React.useId();\n  const [copyStatus, setCopyStatus] = React.useState<CopyStatus>(\"idle\");\n  const [copyFeedbackVisible, setCopyFeedbackVisible] = React.useState(false);\n  const copyTimeoutRef = React.useRef<number | null>(null);\n  const copyResetTimeoutRef = React.useRef<number | null>(null);\n  const hexIsValid = parseHex(hexDraft) !== null;\n\n  React.useEffect(() => {\n    const synchronizedColor = parseColor(selectedValue) ?? FALLBACK_COLOR;\n    setHexDraft(selectedValue);\n    setChannelDraftState((currentState) => {\n      if (\n        activeInput === \"cmyk\" &&\n        currentState.input === activeInput &&\n        channelDraftsMatchColor(currentState.values, activeInput, synchronizedColor)\n      ) {\n        return currentState;\n      }\n\n      return {\n        input: activeInput,\n        values: getChannelDrafts(synchronizedColor, activeInput),\n      };\n    });\n  }, [activeInput, selectedValue]);\n\n  React.useEffect(\n    () => () => {\n      if (copyTimeoutRef.current !== null) {\n        window.clearTimeout(copyTimeoutRef.current);\n      }\n\n      if (copyResetTimeoutRef.current !== null) {\n        window.clearTimeout(copyResetTimeoutRef.current);\n      }\n    },\n    [],\n  );\n\n  const updateColor = (nextColor: Color) => {\n    const normalizedColor = normalizeColor(nextColor);\n\n    if (value === undefined) {\n      setUncontrolledColor(normalizedColor);\n    }\n\n    onValueChange?.(serializeColor(normalizedColor));\n  };\n\n  const commitHexDraft = () => {\n    const nextColor = parseHex(hexDraft);\n\n    if (nextColor) {\n      updateColor(nextColor);\n      setHexDraft(serializeColor(nextColor));\n      return;\n    }\n\n    setHexDraft(selectedValue);\n  };\n\n  const updateChannel = (channel: ChannelName, nextDraft: string) => {\n    const nextDrafts = { ...channelDrafts, [channel]: nextDraft };\n    setChannelDraftState({ input: activeInput, values: nextDrafts });\n\n    if (!activeInput || activeInput === \"hex\") {\n      return;\n    }\n\n    const nextColor = channelDraftsToColor(nextDrafts, activeInput, selectedColor);\n\n    if (nextColor) {\n      updateColor(nextColor);\n    }\n  };\n\n  const resetChannelDrafts = () => {\n    if (\n      activeInput === \"cmyk\" &&\n      channelDraftsMatchColor(channelDrafts, activeInput, selectedColor)\n    ) {\n      return;\n    }\n\n    setChannelDraftState({\n      input: activeInput,\n      values: getChannelDrafts(selectedColor, activeInput),\n    });\n  };\n\n  const selectInput = (nextInput: ColorPickerInput) => {\n    if (nextInput === activeInput) {\n      return;\n    }\n\n    const nextColor = inputSupportsAlpha(nextInput) ? selectedColor : { ...selectedColor, a: 1 };\n    const nextValue = serializeColor(nextColor);\n\n    setHexDraft(nextValue);\n    setChannelDraftState({\n      input: nextInput,\n      values: getChannelDrafts(nextColor, nextInput),\n    });\n    setSelectedInput(nextInput);\n\n    if (nextColor.a !== selectedColor.a) {\n      updateColor(nextColor);\n    }\n  };\n\n  const copyDisplayedValue = async () => {\n    if (copyTimeoutRef.current !== null) {\n      window.clearTimeout(copyTimeoutRef.current);\n    }\n\n    if (copyResetTimeoutRef.current !== null) {\n      window.clearTimeout(copyResetTimeoutRef.current);\n    }\n\n    try {\n      await navigator.clipboard.writeText(displayedValue);\n      setCopyStatus(\"success\");\n    } catch {\n      setCopyStatus(\"error\");\n    }\n\n    setCopyFeedbackVisible(true);\n    copyTimeoutRef.current = window.setTimeout(() => {\n      setCopyFeedbackVisible(false);\n      copyResetTimeoutRef.current = window.setTimeout(\n        () => setCopyStatus(\"idle\"),\n        COPY_FEEDBACK_FADE_DURATION,\n      );\n    }, COPY_FEEDBACK_DURATION);\n  };\n\n  const invalid = props[\"aria-invalid\"] === true || props[\"aria-invalid\"] === \"true\";\n\n  return (\n    <PopoverPrimitive.Root defaultOpen={defaultOpen} open={open} onOpenChange={onOpenChange}>\n      {children ? (\n        <PopoverPrimitive.Trigger\n          render={\n            <button\n              type=\"button\"\n              data-slot=\"color-picker-trigger\"\n              className={cn(\n                \"flex h-10 w-full min-w-0 cursor-pointer items-center gap-2 border border-input bg-background px-3 text-left text-sm outline-none transition-[background-color,border-color,box-shadow,color] duration-100 hover:border-foreground/40 focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40 disabled:cursor-not-allowed disabled:bg-muted disabled:text-muted-foreground aria-invalid:border-destructive aria-invalid:focus-visible:border-destructive aria-invalid:focus-visible:ring-destructive/25 motion-reduce:transition-none\",\n                className,\n              )}\n              disabled={disabled}\n              {...props}\n            />\n          }\n        >\n          {children}\n        </PopoverPrimitive.Trigger>\n      ) : (\n        <div\n          data-slot=\"color-picker-control\"\n          className={cn(\n            \"group/color-picker flex h-10 w-full min-w-0 border border-input bg-background transition-[background-color,border-color,box-shadow,color] duration-100 hover:border-foreground/40 has-[:focus-visible]:border-ring has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-ring/40 motion-reduce:transition-none\",\n            disabled && \"bg-muted text-muted-foreground\",\n            invalid &&\n              \"border-destructive has-[:focus-visible]:border-destructive has-[:focus-visible]:ring-destructive/25\",\n            className,\n          )}\n        >\n          <PopoverPrimitive.Trigger\n            render={\n              <button\n                type=\"button\"\n                data-slot=\"color-picker-trigger\"\n                className=\"flex min-w-0 flex-1 cursor-pointer items-center gap-2 px-3 text-left text-sm outline-none disabled:cursor-not-allowed\"\n                disabled={disabled}\n                {...props}\n              />\n            }\n          >\n            <ColorSwatch color={selectedColor} className=\"size-4 shrink-0\" />\n            <span className=\"min-w-0 flex-1 truncate font-mono text-xs uppercase tabular-nums\">\n              {displayedValue}\n            </span>\n          </PopoverPrimitive.Trigger>\n          <button\n            type=\"button\"\n            data-slot=\"color-picker-copy\"\n            className={cn(\n              \"pointer-events-none inline-flex w-10 shrink-0 cursor-pointer items-center justify-center border-l border-transparent text-muted-foreground opacity-0 outline-none transition-[background-color,border-color,color,opacity] duration-100 group-hover/color-picker:pointer-events-auto group-hover/color-picker:border-border group-hover/color-picker:opacity-100 hover:bg-muted hover:text-foreground focus-visible:z-10 focus-visible:border-border focus-visible:bg-muted focus-visible:text-foreground focus-visible:opacity-100 [@media(hover:none)]:pointer-events-auto [@media(hover:none)]:border-border [@media(hover:none)]:opacity-100 motion-reduce:transition-none\",\n              copyStatus !== \"idle\" && copyFeedbackVisible && \"border-border opacity-100\",\n              copyStatus === \"success\" && \"text-foreground\",\n              copyStatus === \"error\" && \"text-destructive\",\n            )}\n            aria-label={\n              copyStatus === \"success\"\n                ? `${displayFormat.toUpperCase()} color copied`\n                : copyStatus === \"error\"\n                  ? `Could not copy ${displayFormat.toUpperCase()} color. Try again`\n                  : `Copy ${displayFormat.toUpperCase()} color`\n            }\n            onClick={copyDisplayedValue}\n          >\n            {copyStatus === \"success\" ? (\n              <Check className=\"size-3.5\" strokeWidth={2} aria-hidden=\"true\" />\n            ) : copyStatus === \"error\" ? (\n              <TriangleAlert className=\"size-3.5\" strokeWidth={1.75} aria-hidden=\"true\" />\n            ) : (\n              <Copy className=\"size-3.5\" strokeWidth={1.75} aria-hidden=\"true\" />\n            )}\n            <span className=\"sr-only\" aria-live=\"polite\">\n              {copyStatus === \"success\"\n                ? `${displayFormat.toUpperCase()} color copied to clipboard`\n                : copyStatus === \"error\"\n                  ? `Could not copy ${displayFormat.toUpperCase()} color`\n                  : \"\"}\n            </span>\n          </button>\n        </div>\n      )}\n\n      <PopoverPrimitive.Portal>\n        <PopoverPrimitive.Positioner\n          align=\"start\"\n          side=\"bottom\"\n          sideOffset={6}\n          className=\"isolate z-50\"\n        >\n          <PopoverPrimitive.Popup\n            data-slot=\"color-picker-content\"\n            className=\"w-[min(19rem,var(--available-width))] border border-border bg-popover text-popover-foreground outline-none transition-opacity duration-100 data-ending-style:opacity-0 data-instant:transition-none data-starting-style:opacity-0 motion-reduce:transition-none\"\n          >\n            <PopoverPrimitive.Title className=\"sr-only\">Choose color</PopoverPrimitive.Title>\n\n            {activeInput ? (\n              <>\n                <div className=\"border-b border-border p-3\">\n                  <div className=\"mb-2 flex items-center justify-between gap-4\">\n                    <span\n                      id={`${colorInputId}-label`}\n                      className=\"font-mono text-[0.625rem] font-medium uppercase tracking-[0.14em]\"\n                    >\n                      Custom color\n                    </span>\n                    <span className=\"font-mono text-[0.625rem] tabular-nums text-muted-foreground\">\n                      {selectedColor.r} {selectedColor.g} {selectedColor.b}\n                      {selectedColor.a < 1 ? ` / ${formatNumber(selectedColor.a, 2)}` : null}\n                    </span>\n                  </div>\n                  <ColorSurface\n                    id={colorInputId}\n                    labelledBy={`${colorInputId}-label`}\n                    color={selectedColor}\n                    disabled={disabled}\n                    onChange={updateColor}\n                  />\n                </div>\n\n                <div className=\"p-3\">\n                  {enabledInputs.length > 1 ? (\n                    <fieldset className=\"mb-3\">\n                      <legend className=\"sr-only\">Color input format</legend>\n                      <div className=\"flex flex-wrap gap-px border border-border bg-border\">\n                        {enabledInputs.map((input) => (\n                          <button\n                            key={input}\n                            type=\"button\"\n                            aria-pressed={activeInput === input}\n                            className={cn(\n                              \"h-8 min-w-[3.5rem] flex-1 basis-[calc(25%-1px)] cursor-pointer bg-popover px-2 font-mono text-[0.625rem] uppercase tracking-[0.12em] text-muted-foreground outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-ring/40 motion-reduce:transition-none\",\n                              activeInput === input &&\n                                \"bg-foreground text-background hover:bg-foreground hover:text-background\",\n                            )}\n                            onClick={() => selectInput(input)}\n                          >\n                            {input}\n                          </button>\n                        ))}\n                      </div>\n                    </fieldset>\n                  ) : null}\n\n                  {activeInput === \"hex\" ? (\n                    <HexInput\n                      id={hexInputId}\n                      value={hexDraft}\n                      valid={hexIsValid}\n                      disabled={disabled}\n                      onValueChange={(nextDraft) => {\n                        const uppercaseDraft = nextDraft.toUpperCase();\n                        setHexDraft(uppercaseDraft);\n\n                        const nextColor = parseHex(uppercaseDraft);\n\n                        if (nextColor) {\n                          updateColor(nextColor);\n                        }\n                      }}\n                      onCommit={commitHexDraft}\n                    />\n                  ) : (\n                    <ChannelInputs\n                      input={activeInput}\n                      drafts={channelDrafts}\n                      disabled={disabled}\n                      onChange={updateChannel}\n                      onBlur={resetChannelDrafts}\n                    />\n                  )}\n                </div>\n              </>\n            ) : null}\n\n            {normalizedPresets.length > 0 ? (\n              <div className={cn(\"p-3\", activeInput && \"border-t border-border\")}>\n                <p className=\"mb-2 font-mono text-[0.625rem] font-medium uppercase tracking-[0.14em]\">\n                  Presets\n                </p>\n                <div className=\"grid grid-cols-6 gap-2\">\n                  {normalizedPresets.map((presetColor) => {\n                    const presetValue = serializeColor(presetColor);\n\n                    return (\n                      <button\n                        key={presetValue}\n                        type=\"button\"\n                        aria-label={`Use color ${presetValue}`}\n                        aria-pressed={selectedValue === presetValue}\n                        className=\"relative aspect-square cursor-pointer border border-foreground/20 outline-none transition-[border-color,box-shadow] duration-100 hover:border-foreground focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40 disabled:cursor-not-allowed motion-reduce:transition-none\"\n                        onClick={() => updateColor(presetColor)}\n                        disabled={disabled}\n                      >\n                        <ColorSwatch\n                          color={presetColor}\n                          className=\"pointer-events-none absolute inset-0 border-0\"\n                        />\n                        {selectedValue === presetValue ? (\n                          <Check\n                            className={cn(\n                              \"absolute inset-0 z-10 m-auto size-3.5\",\n                              getContrastColor(presetColor) === \"dark\"\n                                ? \"text-black\"\n                                : \"text-white\",\n                            )}\n                            strokeWidth={3}\n                            aria-hidden=\"true\"\n                          />\n                        ) : null}\n                      </button>\n                    );\n                  })}\n                </div>\n              </div>\n            ) : null}\n\n            {!activeInput && normalizedPresets.length === 0 ? (\n              <PopoverPrimitive.Description className=\"p-3 text-sm text-muted-foreground\">\n                No color options configured.\n              </PopoverPrimitive.Description>\n            ) : null}\n          </PopoverPrimitive.Popup>\n        </PopoverPrimitive.Positioner>\n      </PopoverPrimitive.Portal>\n\n      {name ? <input type=\"hidden\" name={name} value={selectedValue} disabled={disabled} /> : null}\n    </PopoverPrimitive.Root>\n  );\n}\n\nfunction ColorSwatch({ className, color }: { className?: string; color: Color }) {\n  return (\n    <span\n      className={cn(\"relative overflow-hidden border border-foreground/20\", className)}\n      aria-hidden=\"true\"\n    >\n      <span className=\"absolute inset-0\" style={TRANSPARENCY_GRID_STYLE} aria-hidden=\"true\" />\n      <span\n        className=\"absolute inset-0\"\n        style={{ backgroundColor: toCssColor(color) }}\n        aria-hidden=\"true\"\n      />\n    </span>\n  );\n}\n\nfunction ColorSurface({\n  color,\n  disabled,\n  id,\n  labelledBy,\n  onChange,\n}: {\n  color: Color;\n  disabled?: boolean;\n  id: string;\n  labelledBy: string;\n  onChange: (color: Color) => void;\n}) {\n  const hsv = rgbToHsv(color);\n  const calculatedSaturation = hsv.s ?? 0;\n  const calculatedBrightness = hsv.v ?? 0;\n  const calculatedHue = hsv.h ?? 0;\n  const hueRef = React.useRef(calculatedHue);\n  const saturationRef = React.useRef(calculatedSaturation);\n  const brightnessRef = React.useRef(calculatedBrightness);\n  const lastEmittedColorRef = React.useRef<string | null>(null);\n  const colorValue = serializeColor(color);\n\n  if (colorValue !== lastEmittedColorRef.current) {\n    if (calculatedSaturation > 0) {\n      hueRef.current = calculatedHue;\n    }\n\n    saturationRef.current = calculatedSaturation;\n    brightnessRef.current = calculatedBrightness;\n  }\n\n  const hue = hueRef.current;\n  const saturation = saturationRef.current;\n  const brightness = brightnessRef.current;\n  const surfaceRef = React.useRef<HTMLDivElement>(null);\n  const markerRef = React.useRef<HTMLSpanElement>(null);\n  const pointerBoundsRef = React.useRef<DOMRect | null>(null);\n  const animationFrameRef = React.useRef<number | null>(null);\n  const lastChangeTimeRef = React.useRef(0);\n  const pointerPositionRef = React.useRef<{ brightness: number; saturation: number } | null>(null);\n  const pendingColorRef = React.useRef<Color | null>(null);\n  const onChangeRef = React.useRef(onChange);\n  onChangeRef.current = onChange;\n\n  const emitColor = (nextColor: Color) => {\n    const normalizedColor = normalizeColor(nextColor);\n    lastEmittedColorRef.current = serializeColor(normalizedColor);\n    onChangeRef.current(normalizedColor);\n  };\n\n  const positionMarker = (nextSaturation: number, nextBrightness: number) => {\n    if (!markerRef.current) {\n      return;\n    }\n\n    markerRef.current.style.left = `${nextSaturation}%`;\n    markerRef.current.style.top = `${100 - nextBrightness}%`;\n  };\n\n  React.useLayoutEffect(() => {\n    const pointerPosition = pointerPositionRef.current;\n\n    if (pointerBoundsRef.current && pointerPosition) {\n      if (markerRef.current) {\n        markerRef.current.style.left = `${pointerPosition.saturation}%`;\n        markerRef.current.style.top = `${100 - pointerPosition.brightness}%`;\n      }\n      return;\n    }\n\n    if (markerRef.current) {\n      markerRef.current.style.left = `${saturation}%`;\n      markerRef.current.style.top = `${100 - brightness}%`;\n    }\n  }, [brightness, saturation]);\n\n  React.useEffect(\n    () => () => {\n      if (animationFrameRef.current !== null) {\n        cancelAnimationFrame(animationFrameRef.current);\n      }\n    },\n    [],\n  );\n\n  const commitScheduledChange = (time: number) => {\n    if (time - lastChangeTimeRef.current < COLOR_DRAG_UPDATE_INTERVAL) {\n      animationFrameRef.current = requestAnimationFrame(commitScheduledChange);\n      return;\n    }\n\n    animationFrameRef.current = null;\n    const pendingColor = pendingColorRef.current;\n    pendingColorRef.current = null;\n\n    if (pendingColor) {\n      lastChangeTimeRef.current = time;\n      emitColor(pendingColor);\n    }\n  };\n\n  const scheduleChange = (nextColor: Color) => {\n    pendingColorRef.current = nextColor;\n\n    if (animationFrameRef.current === null) {\n      animationFrameRef.current = requestAnimationFrame(commitScheduledChange);\n    }\n  };\n\n  const flushPendingChange = () => {\n    if (animationFrameRef.current !== null) {\n      cancelAnimationFrame(animationFrameRef.current);\n      animationFrameRef.current = null;\n    }\n\n    const pendingColor = pendingColorRef.current;\n    pendingColorRef.current = null;\n\n    if (pendingColor) {\n      lastChangeTimeRef.current = performance.now();\n      emitColor(pendingColor);\n    }\n  };\n\n  const updateSaturationAndBrightness = (nextSaturation: number, nextBrightness: number) => {\n    saturationRef.current = nextSaturation;\n    brightnessRef.current = nextBrightness;\n    positionMarker(nextSaturation, nextBrightness);\n    emitColor(hsvToRgb(hueRef.current, nextSaturation, nextBrightness, color.a));\n  };\n\n  const updateFromPointer = (event: React.PointerEvent<HTMLDivElement>) => {\n    if (disabled) {\n      return;\n    }\n\n    const bounds = pointerBoundsRef.current ?? event.currentTarget.getBoundingClientRect();\n    const nextSaturation = clamp(((event.clientX - bounds.left) / bounds.width) * 100, 0, 100);\n    const nextBrightness = clamp(\n      100 - ((event.clientY - bounds.top) / bounds.height) * 100,\n      0,\n      100,\n    );\n\n    pointerPositionRef.current = {\n      brightness: nextBrightness,\n      saturation: nextSaturation,\n    };\n    saturationRef.current = nextSaturation;\n    brightnessRef.current = nextBrightness;\n    positionMarker(nextSaturation, nextBrightness);\n\n    scheduleChange(hsvToRgb(hueRef.current, nextSaturation, nextBrightness, color.a));\n  };\n\n  return (\n    <div>\n      <fieldset\n        id={id}\n        aria-labelledby={labelledBy}\n        data-slot=\"color-picker-surface-controls\"\n        className=\"relative min-w-0 border-0 p-0\"\n        disabled={disabled}\n      >\n        <label htmlFor={`${id}-saturation`} className=\"sr-only\">\n          Saturation\n        </label>\n        <input\n          id={`${id}-saturation`}\n          type=\"range\"\n          min={0}\n          max={100}\n          step={1}\n          value={saturation}\n          aria-valuetext={`${Math.round(saturation)}% saturation`}\n          className=\"peer/saturation sr-only\"\n          onChange={(event) =>\n            updateSaturationAndBrightness(Number(event.target.value), brightness)\n          }\n        />\n        <label htmlFor={`${id}-brightness`} className=\"sr-only\">\n          Brightness\n        </label>\n        <input\n          id={`${id}-brightness`}\n          type=\"range\"\n          min={0}\n          max={100}\n          step={1}\n          value={brightness}\n          aria-valuetext={`${Math.round(brightness)}% brightness`}\n          className=\"peer/brightness sr-only\"\n          onChange={(event) =>\n            updateSaturationAndBrightness(saturation, Number(event.target.value))\n          }\n        />\n        <div\n          ref={surfaceRef}\n          data-slot=\"color-picker-surface\"\n          className={cn(\n            \"relative h-28 w-full touch-none cursor-crosshair overflow-hidden outline-none select-none peer-focus-visible/saturation:ring-2 peer-focus-visible/saturation:ring-ring/40 peer-focus-visible/brightness:ring-2 peer-focus-visible/brightness:ring-ring/40\",\n            disabled && \"cursor-not-allowed opacity-50\",\n          )}\n          style={{\n            backgroundColor: `hsl(${hue} 100% 50%)`,\n            backgroundImage:\n              \"linear-gradient(to top, rgb(0 0 0), transparent), linear-gradient(to right, rgb(255 255 255), transparent)\",\n          }}\n          aria-hidden=\"true\"\n          onPointerDown={(event) => {\n            if (disabled) {\n              return;\n            }\n\n            pointerBoundsRef.current = event.currentTarget.getBoundingClientRect();\n            event.currentTarget.setPointerCapture(event.pointerId);\n            updateFromPointer(event);\n          }}\n          onPointerMove={(event) => {\n            if (event.currentTarget.hasPointerCapture(event.pointerId)) {\n              updateFromPointer(event);\n            }\n          }}\n          onPointerUp={(event) => {\n            if (event.currentTarget.hasPointerCapture(event.pointerId)) {\n              updateFromPointer(event);\n              flushPendingChange();\n              event.currentTarget.releasePointerCapture(event.pointerId);\n            }\n\n            pointerBoundsRef.current = null;\n            pointerPositionRef.current = null;\n          }}\n          onPointerCancel={(event) => {\n            flushPendingChange();\n\n            if (event.currentTarget.hasPointerCapture(event.pointerId)) {\n              event.currentTarget.releasePointerCapture(event.pointerId);\n            }\n\n            pointerBoundsRef.current = null;\n            pointerPositionRef.current = null;\n          }}\n        >\n          <span\n            ref={markerRef}\n            className=\"pointer-events-none absolute size-3 -translate-x-1/2 -translate-y-1/2 border-2 border-white outline outline-1 outline-black/60\"\n            style={{ left: `${saturation}%`, top: `${100 - brightness}%` }}\n            aria-hidden=\"true\"\n          />\n          <span\n            className=\"pointer-events-none absolute inset-0 border border-foreground/20\"\n            aria-hidden=\"true\"\n          />\n        </div>\n        <span\n          className=\"pointer-events-none absolute bottom-2 left-2 border border-border bg-background/90 px-1.5 py-1 font-mono text-[0.625rem] uppercase tracking-[0.1em] opacity-0 peer-focus-visible/saturation:opacity-100\"\n          aria-hidden=\"true\"\n        >\n          Saturation {Math.round(saturation)}%\n        </span>\n        <span\n          className=\"pointer-events-none absolute bottom-2 left-2 border border-border bg-background/90 px-1.5 py-1 font-mono text-[0.625rem] uppercase tracking-[0.1em] opacity-0 peer-focus-visible/brightness:opacity-100\"\n          aria-hidden=\"true\"\n        >\n          Brightness {Math.round(brightness)}%\n        </span>\n      </fieldset>\n\n      <div className=\"relative mt-2 h-5\">\n        <div\n          className=\"pointer-events-none absolute inset-x-0 top-1/2 h-2 -translate-y-1/2 border border-foreground/20\"\n          style={{\n            background:\n              \"linear-gradient(to right, #ff0000, #ffff00, #00ff00, #00ffff, #0000ff, #ff00ff, #ff0000)\",\n          }}\n          aria-hidden=\"true\"\n        />\n        <label htmlFor={`${id}-hue`} className=\"sr-only\">\n          Hue\n        </label>\n        <input\n          id={`${id}-hue`}\n          type=\"range\"\n          min={0}\n          max={360}\n          step={1}\n          value={hue}\n          aria-label=\"Hue\"\n          onChange={(event) => {\n            const nextHue = Number(event.target.value);\n            hueRef.current = nextHue;\n\n            if (surfaceRef.current) {\n              surfaceRef.current.style.backgroundColor = `hsl(${nextHue} 100% 50%)`;\n            }\n\n            scheduleChange(hsvToRgb(nextHue, saturation, brightness, color.a));\n          }}\n          onPointerUp={flushPendingChange}\n          onPointerCancel={flushPendingChange}\n          onBlur={flushPendingChange}\n          className=\"absolute inset-0 h-full w-full cursor-pointer appearance-none bg-transparent outline-none focus-visible:ring-2 focus-visible:ring-ring/40 disabled:cursor-not-allowed disabled:opacity-50 [&::-moz-range-thumb]:size-4 [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:border [&::-moz-range-thumb]:border-foreground [&::-moz-range-thumb]:bg-background [&::-moz-range-thumb]:border-solid [&::-moz-range-track]:h-2 [&::-moz-range-track]:bg-transparent [&::-webkit-slider-runnable-track]:h-2 [&::-webkit-slider-runnable-track]:bg-transparent [&::-webkit-slider-thumb]:mt-[-4px] [&::-webkit-slider-thumb]:size-4 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:border [&::-webkit-slider-thumb]:border-foreground [&::-webkit-slider-thumb]:bg-background\"\n          disabled={disabled}\n        />\n      </div>\n    </div>\n  );\n}\n\nfunction HexInput({\n  disabled,\n  id,\n  onCommit,\n  onValueChange,\n  valid,\n  value,\n}: {\n  disabled?: boolean;\n  id: string;\n  onCommit: () => void;\n  onValueChange: (value: string) => void;\n  valid: boolean;\n  value: string;\n}) {\n  return (\n    <div>\n      <label\n        htmlFor={id}\n        className=\"mb-2 block font-mono text-[0.625rem] font-medium uppercase tracking-[0.14em]\"\n      >\n        Hex value\n      </label>\n      <input\n        id={id}\n        type=\"text\"\n        value={value}\n        onChange={(event) => onValueChange(event.target.value)}\n        onBlur={onCommit}\n        onKeyDown={(event) => {\n          if (event.key === \"Enter\") {\n            onCommit();\n          }\n        }}\n        maxLength={9}\n        spellCheck=\"false\"\n        autoComplete=\"off\"\n        aria-invalid={!valid}\n        aria-describedby={`${id}-hint`}\n        className=\"h-10 w-full border border-input bg-background px-3 font-mono text-sm uppercase tabular-nums outline-none transition-[border-color,box-shadow] duration-100 focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40 aria-invalid:border-destructive aria-invalid:focus-visible:border-destructive aria-invalid:focus-visible:ring-destructive/25 motion-reduce:transition-none\"\n        disabled={disabled}\n      />\n      <p\n        id={`${id}-hint`}\n        className={cn(\"mt-2 text-xs text-muted-foreground\", !valid && \"text-destructive\")}\n      >\n        Use 3, 4, 6, or 8 hexadecimal digits.\n      </p>\n    </div>\n  );\n}\n\nfunction ChannelInputs({\n  disabled,\n  drafts,\n  input,\n  onBlur,\n  onChange,\n}: {\n  disabled?: boolean;\n  drafts: ChannelDrafts;\n  input: NumericColorPickerInput;\n  onBlur: () => void;\n  onChange: (channel: ChannelName, value: string) => void;\n}) {\n  const definition = INPUT_DEFINITIONS[input];\n  const rangeChannel =\n    input === \"rgba\"\n      ? definition.channels.find((channel) => channel.name === \"a\")\n      : input === \"cmyk\"\n        ? definition.channels.find((channel) => channel.name === \"k\")\n        : undefined;\n\n  return (\n    <fieldset>\n      <legend className=\"mb-2 font-mono text-[0.625rem] font-medium uppercase tracking-[0.14em]\">\n        {definition.label}\n      </legend>\n      <div\n        className={cn(\n          \"grid gap-2\",\n          definition.channels.length === 4 ? \"grid-cols-4\" : \"grid-cols-3\",\n        )}\n      >\n        {definition.channels.map((channel) => {\n          const draft = drafts[channel.name] ?? \"\";\n          const invalid = !isValidChannelDraft(channel, draft);\n\n          return (\n            <label key={channel.name} className=\"min-w-0\">\n              <span className=\"mb-1.5 block font-mono text-[0.625rem] uppercase text-muted-foreground\">\n                {channel.name}\n              </span>\n              <input\n                type=\"number\"\n                inputMode=\"decimal\"\n                min={channel.minimum}\n                max={channel.maximum}\n                step={channel.step}\n                value={draft}\n                onChange={(event) => onChange(channel.name, event.target.value)}\n                onBlur={onBlur}\n                aria-invalid={invalid}\n                className=\"h-10 w-full min-w-0 border border-input bg-background px-2 font-mono text-sm tabular-nums outline-none transition-[border-color,box-shadow] duration-100 focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40 aria-invalid:border-destructive aria-invalid:focus-visible:border-destructive aria-invalid:focus-visible:ring-destructive/25 motion-reduce:transition-none\"\n                disabled={disabled}\n              />\n            </label>\n          );\n        })}\n      </div>\n      {rangeChannel && (input === \"rgba\" || input === \"cmyk\") ? (\n        <ChannelRange\n          channel={rangeChannel}\n          drafts={drafts}\n          input={input}\n          disabled={disabled}\n          onBlur={onBlur}\n          onChange={onChange}\n        />\n      ) : null}\n      <p className=\"mt-2 text-xs text-muted-foreground\">{definition.hint}</p>\n    </fieldset>\n  );\n}\n\nfunction ChannelRange({\n  channel,\n  disabled,\n  drafts,\n  input,\n  onBlur,\n  onChange,\n}: {\n  channel: ChannelDefinition;\n  disabled?: boolean;\n  drafts: ChannelDrafts;\n  input: \"rgba\" | \"cmyk\";\n  onBlur: () => void;\n  onChange: (channel: ChannelName, value: string) => void;\n}) {\n  const rangeId = React.useId();\n  const draft = drafts[channel.name] ?? \"\";\n  const parsedValue = Number(draft);\n  const value = Number.isFinite(parsedValue)\n    ? clamp(parsedValue, channel.minimum, channel.maximum)\n    : channel.minimum;\n  const percentage = ((value - channel.minimum) / (channel.maximum - channel.minimum)) * 100;\n  const label = input === \"rgba\" ? \"Alpha\" : \"Key / Black\";\n  const trackBackground = getChannelRangeBackground(input, drafts);\n\n  return (\n    <div className=\"mt-3 border-t border-border pt-3\">\n      <div className=\"flex items-center justify-between gap-4\">\n        <label\n          htmlFor={rangeId}\n          className=\"font-mono text-[0.625rem] font-medium uppercase tracking-[0.14em]\"\n        >\n          {label}\n        </label>\n        <output\n          htmlFor={rangeId}\n          className=\"font-mono text-[0.625rem] tabular-nums text-muted-foreground\"\n        >\n          {Math.round(percentage)}%\n        </output>\n      </div>\n      <div className=\"relative mt-1 h-5\">\n        <div\n          className=\"pointer-events-none absolute inset-x-0 top-1/2 h-2 -translate-y-1/2 overflow-hidden border border-foreground/20\"\n          style={input === \"rgba\" ? TRANSPARENCY_GRID_STYLE : undefined}\n          aria-hidden=\"true\"\n        >\n          <span\n            className=\"absolute inset-0\"\n            style={{ background: trackBackground }}\n            aria-hidden=\"true\"\n          />\n        </div>\n        <input\n          id={rangeId}\n          type=\"range\"\n          min={channel.minimum}\n          max={channel.maximum}\n          step={channel.step}\n          value={value}\n          aria-label={label}\n          aria-valuetext={`${Math.round(percentage)}% ${label.toLowerCase()}`}\n          onChange={(event) => onChange(channel.name, event.target.value)}\n          onBlur={onBlur}\n          className=\"absolute inset-0 h-full w-full cursor-pointer appearance-none bg-transparent outline-none focus-visible:ring-2 focus-visible:ring-ring/40 disabled:cursor-not-allowed disabled:opacity-50 [&::-moz-range-thumb]:size-4 [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:border [&::-moz-range-thumb]:border-foreground [&::-moz-range-thumb]:bg-background [&::-moz-range-thumb]:border-solid [&::-moz-range-track]:h-2 [&::-moz-range-track]:bg-transparent [&::-webkit-slider-runnable-track]:h-2 [&::-webkit-slider-runnable-track]:bg-transparent [&::-webkit-slider-thumb]:mt-[-4px] [&::-webkit-slider-thumb]:size-4 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:border [&::-webkit-slider-thumb]:border-foreground [&::-webkit-slider-thumb]:bg-background\"\n          disabled={disabled}\n        />\n      </div>\n    </div>\n  );\n}\n\nfunction getChannelRangeBackground(input: \"rgba\" | \"cmyk\", drafts: ChannelDrafts) {\n  if (input === \"rgba\") {\n    const red = clamp(Number(drafts.r) || 0, 0, 255);\n    const green = clamp(Number(drafts.g) || 0, 0, 255);\n    const blue = clamp(Number(drafts.b) || 0, 0, 255);\n\n    return `linear-gradient(to right, rgb(${red} ${green} ${blue} / 0), rgb(${red} ${green} ${blue}))`;\n  }\n\n  const cyan = clamp(Number(drafts.c) || 0, 0, 100);\n  const magenta = clamp(Number(drafts.m) || 0, 0, 100);\n  const yellow = clamp(Number(drafts.y) || 0, 0, 100);\n  const baseColor = cmykToRgb(cyan, magenta, yellow, 0, 1);\n\n  return `linear-gradient(to right, ${toCssColor(baseColor)}, rgb(0 0 0))`;\n}\n\nfunction normalizeInputs(\n  inputs: ColorPickerInput | readonly ColorPickerInput[] | undefined,\n  hasPresets: boolean,\n) {\n  let requestedInputs: readonly ColorPickerInput[];\n\n  if (inputs === undefined) {\n    requestedInputs = hasPresets ? [] : [\"rgb\"];\n  } else {\n    requestedInputs = typeof inputs === \"string\" ? [inputs] : inputs;\n  }\n\n  const normalizedInputs = requestedInputs.filter(\n    (input, index) => requestedInputs.indexOf(input) === index,\n  );\n\n  return normalizedInputs;\n}\n\nfunction inputSupportsAlpha(input: ColorPickerInput) {\n  return input === \"hex\" || input === \"rgba\";\n}\n\nfunction getChannelDrafts(color: Color, input: ColorPickerInput | undefined): ChannelDrafts {\n  if (!input || input === \"hex\") {\n    return {};\n  }\n\n  const channels = colorToChannels(color, input);\n\n  return Object.fromEntries(\n    INPUT_DEFINITIONS[input].channels.map((channel) => [\n      channel.name,\n      formatNumber(channels[channel.name] ?? 0, channel.step < 1 ? 2 : 0),\n    ]),\n  );\n}\n\nfunction colorToChannels(color: Color, input: NumericColorPickerInput): ChannelValues {\n  switch (input) {\n    case \"rgb\":\n      return { r: color.r, g: color.g, b: color.b };\n    case \"rgba\":\n      return { r: color.r, g: color.g, b: color.b, a: color.a };\n    case \"ryb\":\n      return rgbToRyb(color);\n    case \"cmyk\":\n      return rgbToCmyk(color);\n    case \"hsl\":\n      return rgbToHsl(color);\n    case \"hsv\":\n      return rgbToHsv(color);\n  }\n}\n\nfunction channelDraftsToColor(\n  drafts: ChannelDrafts,\n  input: NumericColorPickerInput,\n  fallback: Color,\n) {\n  const definition = INPUT_DEFINITIONS[input];\n\n  if (\n    definition.channels.some((channel) => !isValidChannelDraft(channel, drafts[channel.name] ?? \"\"))\n  ) {\n    return null;\n  }\n\n  const value = (channel: ChannelName) => Number(drafts[channel]);\n\n  switch (input) {\n    case \"rgb\":\n      return normalizeColor({ r: value(\"r\"), g: value(\"g\"), b: value(\"b\"), a: fallback.a });\n    case \"rgba\":\n      return normalizeColor({ r: value(\"r\"), g: value(\"g\"), b: value(\"b\"), a: value(\"a\") });\n    case \"ryb\":\n      return rybToRgb(value(\"r\"), value(\"y\"), value(\"b\"), fallback.a);\n    case \"cmyk\":\n      return cmykToRgb(value(\"c\"), value(\"m\"), value(\"y\"), value(\"k\"), fallback.a);\n    case \"hsl\":\n      return hslToRgb(value(\"h\"), value(\"s\"), value(\"l\"), fallback.a);\n    case \"hsv\":\n      return hsvToRgb(value(\"h\"), value(\"s\"), value(\"v\"), fallback.a);\n  }\n}\n\nfunction channelDraftsMatchColor(\n  drafts: ChannelDrafts,\n  input: NumericColorPickerInput,\n  color: Color,\n) {\n  const draftColor = channelDraftsToColor(drafts, input, color);\n\n  return draftColor !== null && serializeColor(draftColor) === serializeColor(color);\n}\n\nfunction parseColor(value: string) {\n  const hexColor = parseHex(value);\n\n  if (hexColor) {\n    return hexColor;\n  }\n\n  const match = value\n    .trim()\n    .match(\n      /^rgba?\\(\\s*(\\d+(?:\\.\\d+)?)\\s*,\\s*(\\d+(?:\\.\\d+)?)\\s*,\\s*(\\d+(?:\\.\\d+)?)(?:\\s*,\\s*(\\d*\\.?\\d+)\\s*)?\\)$/i,\n    );\n\n  if (!match) {\n    return null;\n  }\n\n  const [, red, green, blue, alpha] = match;\n  const color = {\n    r: Number(red),\n    g: Number(green),\n    b: Number(blue),\n    a: alpha === undefined ? 1 : Number(alpha),\n  };\n\n  if (\n    !Number.isFinite(color.r) ||\n    !Number.isFinite(color.g) ||\n    !Number.isFinite(color.b) ||\n    !Number.isFinite(color.a) ||\n    color.r < 0 ||\n    color.r > 255 ||\n    color.g < 0 ||\n    color.g > 255 ||\n    color.b < 0 ||\n    color.b > 255 ||\n    color.a < 0 ||\n    color.a > 1\n  ) {\n    return null;\n  }\n\n  return normalizeColor(color);\n}\n\nfunction parseHex(value: string) {\n  const normalizedValue = value.trim().replace(/^#/, \"\");\n\n  if (!/^(?:[0-9A-F]{3}|[0-9A-F]{4}|[0-9A-F]{6}|[0-9A-F]{8})$/i.test(normalizedValue)) {\n    return null;\n  }\n\n  const expandedValue =\n    normalizedValue.length <= 4\n      ? normalizedValue\n          .split(\"\")\n          .map((character) => character + character)\n          .join(\"\")\n      : normalizedValue;\n\n  return {\n    r: Number.parseInt(expandedValue.slice(0, 2), 16),\n    g: Number.parseInt(expandedValue.slice(2, 4), 16),\n    b: Number.parseInt(expandedValue.slice(4, 6), 16),\n    a: expandedValue.length === 8 ? Number.parseInt(expandedValue.slice(6, 8), 16) / 255 : 1,\n  };\n}\n\nfunction rgbToRyb(color: Color): ChannelValues {\n  let red = color.r;\n  let green = color.g;\n  let blue = color.b;\n  const white = Math.min(red, green, blue);\n\n  red -= white;\n  green -= white;\n  blue -= white;\n\n  const maximumGreen = Math.max(red, green, blue);\n  let yellow = Math.min(red, green);\n\n  red -= yellow;\n  green -= yellow;\n\n  if (blue > 0 && green > 0) {\n    blue /= 2;\n    green /= 2;\n  }\n\n  yellow += green;\n  blue += green;\n\n  const maximumYellow = Math.max(red, yellow, blue);\n\n  if (maximumYellow > 0) {\n    const normalization = maximumGreen / maximumYellow;\n    red *= normalization;\n    yellow *= normalization;\n    blue *= normalization;\n  }\n\n  return {\n    r: Math.round(red + white),\n    y: Math.round(yellow + white),\n    b: Math.round(blue + white),\n  };\n}\n\nfunction rybToRgb(redValue: number, yellowValue: number, blueValue: number, alpha: number): Color {\n  let red = redValue;\n  let yellow = yellowValue;\n  let blue = blueValue;\n  const white = Math.min(red, yellow, blue);\n\n  red -= white;\n  yellow -= white;\n  blue -= white;\n\n  const maximumYellow = Math.max(red, yellow, blue);\n  let green = Math.min(yellow, blue);\n\n  yellow -= green;\n  blue -= green;\n\n  if (blue > 0 && green > 0) {\n    blue *= 2;\n    green *= 2;\n  }\n\n  red += yellow;\n  green += yellow;\n\n  const maximumGreen = Math.max(red, green, blue);\n\n  if (maximumGreen > 0) {\n    const normalization = maximumYellow / maximumGreen;\n    red *= normalization;\n    green *= normalization;\n    blue *= normalization;\n  }\n\n  return normalizeColor({\n    r: red + white,\n    g: green + white,\n    b: blue + white,\n    a: alpha,\n  });\n}\n\nfunction rgbToCmyk(color: Color): ChannelValues {\n  const red = color.r / 255;\n  const green = color.g / 255;\n  const blue = color.b / 255;\n  const key = 1 - Math.max(red, green, blue);\n\n  if (key >= 1) {\n    return { c: 0, m: 0, y: 0, k: 100 };\n  }\n\n  return {\n    c: Math.round(((1 - red - key) / (1 - key)) * 100),\n    m: Math.round(((1 - green - key) / (1 - key)) * 100),\n    y: Math.round(((1 - blue - key) / (1 - key)) * 100),\n    k: Math.round(key * 100),\n  };\n}\n\nfunction cmykToRgb(\n  cyanValue: number,\n  magentaValue: number,\n  yellowValue: number,\n  keyValue: number,\n  alpha: number,\n): Color {\n  const cyan = cyanValue / 100;\n  const magenta = magentaValue / 100;\n  const yellow = yellowValue / 100;\n  const key = keyValue / 100;\n\n  return normalizeColor({\n    r: 255 * (1 - cyan) * (1 - key),\n    g: 255 * (1 - magenta) * (1 - key),\n    b: 255 * (1 - yellow) * (1 - key),\n    a: alpha,\n  });\n}\n\nfunction rgbToHsl(color: Color): ChannelValues {\n  const red = color.r / 255;\n  const green = color.g / 255;\n  const blue = color.b / 255;\n  const maximum = Math.max(red, green, blue);\n  const minimum = Math.min(red, green, blue);\n  const delta = maximum - minimum;\n  const lightness = (maximum + minimum) / 2;\n  let hue = 0;\n\n  if (delta !== 0) {\n    if (maximum === red) {\n      hue = 60 * (((green - blue) / delta) % 6);\n    } else if (maximum === green) {\n      hue = 60 * ((blue - red) / delta + 2);\n    } else {\n      hue = 60 * ((red - green) / delta + 4);\n    }\n  }\n\n  if (hue < 0) {\n    hue += 360;\n  }\n\n  const saturation = delta === 0 ? 0 : delta / (1 - Math.abs(2 * lightness - 1));\n\n  return {\n    h: Math.round(hue),\n    s: Math.round(saturation * 100),\n    l: Math.round(lightness * 100),\n  };\n}\n\nfunction hslToRgb(\n  hueValue: number,\n  saturationValue: number,\n  lightnessValue: number,\n  alpha: number,\n) {\n  const hue = normalizeHue(hueValue);\n  const saturation = saturationValue / 100;\n  const lightness = lightnessValue / 100;\n  const chroma = (1 - Math.abs(2 * lightness - 1)) * saturation;\n  const secondary = chroma * (1 - Math.abs(((hue / 60) % 2) - 1));\n  const offset = lightness - chroma / 2;\n  const [red, green, blue] = hueToRgbChannels(hue, chroma, secondary);\n\n  return normalizeColor({\n    r: (red + offset) * 255,\n    g: (green + offset) * 255,\n    b: (blue + offset) * 255,\n    a: alpha,\n  });\n}\n\nfunction rgbToHsv(color: Color): ChannelValues {\n  const red = color.r / 255;\n  const green = color.g / 255;\n  const blue = color.b / 255;\n  const maximum = Math.max(red, green, blue);\n  const minimum = Math.min(red, green, blue);\n  const delta = maximum - minimum;\n  let hue = 0;\n\n  if (delta !== 0) {\n    if (maximum === red) {\n      hue = 60 * (((green - blue) / delta) % 6);\n    } else if (maximum === green) {\n      hue = 60 * ((blue - red) / delta + 2);\n    } else {\n      hue = 60 * ((red - green) / delta + 4);\n    }\n  }\n\n  if (hue < 0) {\n    hue += 360;\n  }\n\n  return {\n    h: Math.round(hue),\n    s: Math.round((maximum === 0 ? 0 : delta / maximum) * 100),\n    v: Math.round(maximum * 100),\n  };\n}\n\nfunction hsvToRgb(hueValue: number, saturationValue: number, valueValue: number, alpha: number) {\n  const hue = normalizeHue(hueValue);\n  const saturation = saturationValue / 100;\n  const value = valueValue / 100;\n  const chroma = value * saturation;\n  const secondary = chroma * (1 - Math.abs(((hue / 60) % 2) - 1));\n  const offset = value - chroma;\n  const [red, green, blue] = hueToRgbChannels(hue, chroma, secondary);\n\n  return normalizeColor({\n    r: (red + offset) * 255,\n    g: (green + offset) * 255,\n    b: (blue + offset) * 255,\n    a: alpha,\n  });\n}\n\nfunction hueToRgbChannels(hue: number, chroma: number, secondary: number) {\n  if (hue < 60) {\n    return [chroma, secondary, 0];\n  }\n  if (hue < 120) {\n    return [secondary, chroma, 0];\n  }\n  if (hue < 180) {\n    return [0, chroma, secondary];\n  }\n  if (hue < 240) {\n    return [0, secondary, chroma];\n  }\n  if (hue < 300) {\n    return [secondary, 0, chroma];\n  }\n\n  return [chroma, 0, secondary];\n}\n\nfunction normalizeHue(value: number) {\n  return ((value % 360) + 360) % 360;\n}\n\nfunction normalizeColor(color: Color): Color {\n  return {\n    r: Math.round(clamp(color.r, 0, 255)),\n    g: Math.round(clamp(color.g, 0, 255)),\n    b: Math.round(clamp(color.b, 0, 255)),\n    a: clamp(color.a, 0, 1),\n  };\n}\n\nfunction serializeColor(color: Color) {\n  const normalizedColor = normalizeColor(color);\n  const opaqueHex = toOpaqueHex(normalizedColor);\n  const alphaHex = toHexByte(normalizedColor.a * 255);\n\n  return alphaHex === \"FF\" ? opaqueHex : opaqueHex + alphaHex;\n}\n\nfunction formatDisplayValue(color: Color, format: ColorPickerInput) {\n  if (format === \"hex\") {\n    return serializeColor(color);\n  }\n\n  const channels = colorToChannels(color, format);\n  const value = (channel: ChannelName, precision = 0) =>\n    formatNumber(channels[channel] ?? 0, precision);\n\n  switch (format) {\n    case \"rgb\":\n      return `rgb(${value(\"r\")}, ${value(\"g\")}, ${value(\"b\")})`;\n    case \"rgba\":\n      return `rgba(${value(\"r\")}, ${value(\"g\")}, ${value(\"b\")}, ${value(\"a\", 2)})`;\n    case \"ryb\":\n      return `ryb(${value(\"r\")}, ${value(\"y\")}, ${value(\"b\")})`;\n    case \"cmyk\":\n      return `cmyk(${value(\"c\")}%, ${value(\"m\")}%, ${value(\"y\")}%, ${value(\"k\")}%)`;\n    case \"hsl\":\n      return `hsl(${value(\"h\")}, ${value(\"s\")}%, ${value(\"l\")}%)`;\n    case \"hsv\":\n      return `hsv(${value(\"h\")}, ${value(\"s\")}%, ${value(\"v\")}%)`;\n  }\n}\n\nfunction toOpaqueHex(color: Color) {\n  return `#${toHexByte(color.r)}${toHexByte(color.g)}${toHexByte(color.b)}`;\n}\n\nfunction toCssColor(color: Color) {\n  return `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})`;\n}\n\nfunction toHexByte(value: number) {\n  return Math.round(clamp(value, 0, 255))\n    .toString(16)\n    .padStart(2, \"0\")\n    .toUpperCase();\n}\n\nfunction formatNumber(value: number, precision: number) {\n  return String(Number(value.toFixed(precision)));\n}\n\nfunction isValidChannelDraft(channel: ChannelDefinition, value: string) {\n  if (value.trim() === \"\") {\n    return false;\n  }\n\n  const number = Number(value);\n  return Number.isFinite(number) && number >= channel.minimum && number <= channel.maximum;\n}\n\nfunction getContrastColor(color: Color) {\n  const luminance = (0.299 * color.r + 0.587 * color.g + 0.114 * color.b) / 255;\n\n  return luminance > 0.62 || color.a < 0.45 ? \"dark\" : \"light\";\n}\n\nfunction clamp(value: number, minimum: number, maximum: number) {\n  return Math.min(Math.max(value, minimum), maximum);\n}\n\nexport type { ColorPickerInput, ColorPickerProps };\nexport { ColorPicker };\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}