{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "color-picker",
  "type": "registry:ui",
  "title": "Color Picker",
  "description": "Color input with picker UI, formats, presets, and contrast helpers.",
  "dependencies": [
    "@base-ui/react@^1.7.0"
  ],
  "registryDependencies": [
    "https://control-ui.dev/r/button.json",
    "https://control-ui.dev/r/core.json",
    "https://control-ui.dev/r/input.json",
    "https://control-ui.dev/r/number-field.json",
    "https://control-ui.dev/r/select.json",
    "https://control-ui.dev/r/surface-variants.json"
  ],
  "files": [
    {
      "path": "src/registry/hooks/use-color-area.ts",
      "target": "@components/control-ui/hooks/use-color-area.ts",
      "type": "registry:hook",
      "content": "\"use client\";\n\nimport type { PointerEvent as ReactPointerEvent, RefObject } from \"react\";\nimport { useEffect, useEffectEvent, useRef, useState } from \"react\";\n\nexport type ColorAreaOffset = { x: number; y: number };\nexport type ColorAreaRect = { width: number; height: number };\n\nexport type ColorAreaPointer = {\n  areaRef: RefObject<HTMLDivElement | null>;\n  dragging: boolean;\n  onPointerDown: (event: ReactPointerEvent) => void;\n};\n\n// window listeners rather than pointer capture, so pointerdown can emit immediately and gated effect owns cleanup\nexport function useColorArea(onChange: (offset: ColorAreaOffset, rect: ColorAreaRect) => void): ColorAreaPointer {\n  const areaRef = useRef<HTMLDivElement | null>(null);\n  const [dragging, setDragging] = useState(false);\n  const emitDragChange = useEffectEvent(onChange);\n\n  useEffect(() => {\n    if (!dragging) return;\n\n    function emit(clientX: number, clientY: number) {\n      const el = areaRef.current;\n      if (!el) return;\n      const rect = el.getBoundingClientRect();\n      emitDragChange({ x: clientX - rect.left, y: clientY - rect.top }, { width: rect.width, height: rect.height });\n    }\n    function onMove(event: globalThis.PointerEvent) {\n      emit(event.clientX, event.clientY);\n    }\n    function onUp() {\n      setDragging(false);\n    }\n\n    const previousSelect = document.body.style.userSelect;\n    document.body.style.userSelect = \"none\";\n    window.addEventListener(\"pointermove\", onMove);\n    window.addEventListener(\"pointerup\", onUp);\n\n    return () => {\n      window.removeEventListener(\"pointermove\", onMove);\n      window.removeEventListener(\"pointerup\", onUp);\n      document.body.style.userSelect = previousSelect;\n    };\n  }, [dragging]);\n\n  function onPointerDown(event: ReactPointerEvent) {\n    const el = areaRef.current;\n    if (el) {\n      const rect = el.getBoundingClientRect();\n      onChange({ x: event.clientX - rect.left, y: event.clientY - rect.top }, { width: rect.width, height: rect.height });\n    }\n    setDragging(true);\n    event.preventDefault();\n  }\n\n  return { areaRef, dragging, onPointerDown };\n}\n"
    },
    {
      "path": "src/registry/knob-contracts/button-knobs.ts",
      "target": "@components/control-ui/knob-contracts/button-knobs.ts",
      "type": "registry:component",
      "content": "// Generated from src/registry/sources/control-ui/recipes/button.css by scripts/gen-knob-contracts.ts — run `bun run sync:knobs`.\nexport const buttonKnobs = [\n  \"--cui-button-radius\",\n  \"--cui-button-gap\",\n  \"--cui-button-icon\",\n  \"--cui-button-bg\",\n  \"--cui-button-bg-image\",\n  \"--cui-button-foreground\",\n  \"--cui-button-hover-bg\",\n  \"--cui-button-hover-foreground\",\n  \"--cui-button-press-bg\",\n  \"--cui-button-press-scale\",\n  \"--cui-button-active-bg\",\n  \"--cui-button-active-foreground\",\n  \"--cui-button-active-hover-bg\",\n  \"--cui-button-shadow\",\n  \"--cui-button-hover-shadow\",\n  \"--cui-button-press-shadow\",\n  \"--cui-button-active-shadow\",\n] as const;\nexport type ButtonKnobStyle = Partial<Record<(typeof buttonKnobs)[number], string>>;\n"
    },
    {
      "path": "src/registry/knob-contracts/color-picker-knobs.ts",
      "target": "@components/control-ui/knob-contracts/color-picker-knobs.ts",
      "type": "registry:component",
      "content": "// Generated from src/registry/sources/control-ui/recipes/color-picker.css by scripts/gen-knob-contracts.ts — run `bun run sync:knobs`.\nexport const colorPickerKnobs = [\n  \"--cui-color-picker-output-swatch-radius\",\n  \"--cui-color-picker-swatch-radius\",\n  \"--cui-color-picker-area-radius\",\n  \"--cui-color-picker-trigger-radius\",\n  \"--cui-color-picker-trigger-shadow\",\n  \"--cui-color-picker-slider-thumb-radius\",\n  \"--cui-color-picker-slider-thumb-background\",\n] as const;\nexport type ColorPickerKnobStyle = Partial<Record<(typeof colorPickerKnobs)[number], string>>;\n"
    },
    {
      "path": "src/registry/knob-contracts/popup-knobs.ts",
      "target": "@components/control-ui/knob-contracts/popup-knobs.ts",
      "type": "registry:component",
      "content": "// Generated from src/registry/sources/control-ui/recipes/popup.css by scripts/gen-knob-contracts.ts — run `bun run sync:knobs`.\nexport const popupKnobs = [\n  \"--cui-popup-radius\",\n  \"--cui-popup-background\",\n  \"--cui-popup-foreground\",\n  \"--cui-popup-border-color\",\n  \"--cui-popup-border-width\",\n  \"--cui-popup-shadow\",\n  \"--cui-popup-backdrop-filter\",\n  \"--cui-popup-backdrop-background\",\n  \"--cui-popup-item-radius\",\n  \"--cui-popup-item-foreground\",\n  \"--cui-popup-item-highlight-background\",\n  \"--cui-popup-item-highlight-foreground\",\n  \"--cui-popup-item-highlight-muted-foreground\",\n  \"--cui-popup-item-disabled-opacity\",\n  \"--cui-popup-separator-color\",\n  \"--cui-popup-shortcut-foreground\",\n] as const;\nexport type PopupKnobStyle = Partial<Record<(typeof popupKnobs)[number], string>>;\n"
    },
    {
      "path": "src/registry/lib/color.ts",
      "target": "@components/control-ui/lib/color.ts",
      "type": "registry:lib",
      "content": "// HSVA (h 0-360, s/v 0-100, a 0-1) is source of truth: hue and saturation are indeterminate at black, white, and gray,\n// so state stays HSVA across interaction and collapses to CSS string only for I/O.\nexport type Hsva = { h: number; s: number; v: number; a: number };\nexport type Rgba = { r: number; g: number; b: number; a: number };\nexport type Hsla = { h: number; s: number; l: number; a: number };\nexport type Oklcha = { L: number; C: number; H: number; a: number };\nexport type ColorFormat = \"hex\" | \"rgb\" | \"hsl\" | \"oklch\";\n\n// tagged by color space so numeric fields stay exhaustive without casts\nexport type ChannelId = \"r\" | \"g\" | \"b\" | \"h\" | \"s\" | \"l\" | \"okl\" | \"okc\" | \"okh\" | \"a\";\n\nexport type ChannelSpec = {\n  id: ChannelId;\n  label: string;\n  value: number;\n  min: number;\n  max: number;\n  step: number;\n};\n\nconst clamp = (n: number, min: number, max: number) => Math.min(max, Math.max(min, n));\nconst clamp01 = (n: number) => clamp(n, 0, 1);\nconst wrapHue = (h: number) => ((h % 360) + 360) % 360;\nconst round = (n: number) => Math.round(n);\n\nexport function hsvaToRgba({ h, s, v, a }: Hsva): Rgba {\n  const S = clamp01(s / 100);\n  const V = clamp01(v / 100);\n  const c = V * S;\n  const hh = wrapHue(h) / 60;\n  const x = c * (1 - Math.abs((hh % 2) - 1));\n  const m = V - c;\n  let r = 0;\n  let g = 0;\n  let b = 0;\n  if (hh < 1) [r, g, b] = [c, x, 0];\n  else if (hh < 2) [r, g, b] = [x, c, 0];\n  else if (hh < 3) [r, g, b] = [0, c, x];\n  else if (hh < 4) [r, g, b] = [0, x, c];\n  else if (hh < 5) [r, g, b] = [x, 0, c];\n  else [r, g, b] = [c, 0, x];\n  return { r: round((r + m) * 255), g: round((g + m) * 255), b: round((b + m) * 255), a: clamp01(a) };\n}\n\nexport function rgbaToHsva({ r, g, b, a }: Rgba): Hsva {\n  const R = clamp(r, 0, 255) / 255;\n  const G = clamp(g, 0, 255) / 255;\n  const B = clamp(b, 0, 255) / 255;\n  const max = Math.max(R, G, B);\n  const min = Math.min(R, G, B);\n  const d = max - min;\n  let h = 0;\n  if (d !== 0) {\n    if (max === R) h = 60 * (((G - B) / d) % 6);\n    else if (max === G) h = 60 * ((B - R) / d + 2);\n    else h = 60 * ((R - G) / d + 4);\n  }\n  const s = max === 0 ? 0 : d / max;\n  return { h: wrapHue(h), s: s * 100, v: max * 100, a: clamp01(a) };\n}\n\n// hue carried verbatim so grays keep theirs\nexport function hsvaToHsla({ h, s, v, a }: Hsva): Hsla {\n  const S = clamp01(s / 100);\n  const V = clamp01(v / 100);\n  const L = V * (1 - S / 2);\n  const Sl = L === 0 || L === 1 ? 0 : (V - L) / Math.min(L, 1 - L);\n  return { h: wrapHue(h), s: Sl * 100, l: L * 100, a: clamp01(a) };\n}\n\nexport function hslaToHsva({ h, s, l, a }: Hsla): Hsva {\n  const S = clamp01(s / 100);\n  const L = clamp01(l / 100);\n  const V = L + S * Math.min(L, 1 - L);\n  const Sv = V === 0 ? 0 : 2 * (1 - L / V);\n  return { h: wrapHue(h), s: Sv * 100, v: V * 100, a: clamp01(a) };\n}\n\nexport function rgbaToHsla(rgba: Rgba): Hsla {\n  return hsvaToHsla(rgbaToHsva(rgba));\n}\n\nexport function hslaToRgba(hsla: Hsla): Rgba {\n  return hsvaToRgba(hslaToHsva(hsla));\n}\n\nconst srgbToLinear = (c: number) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);\nconst linearToSrgb = (c: number) => (c <= 0.0031308 ? 12.92 * c : 1.055 * c ** (1 / 2.4) - 0.055);\n\n// rgb channels 0–255 → oklch (L 0–1, C ~0–0.4, H 0–360)\nfunction rgbToOklch(r255: number, g255: number, b255: number): { L: number; C: number; H: number } {\n  const r = srgbToLinear(clamp(r255, 0, 255) / 255);\n  const g = srgbToLinear(clamp(g255, 0, 255) / 255);\n  const b = srgbToLinear(clamp(b255, 0, 255) / 255);\n  const l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;\n  const m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;\n  const s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;\n  const l_ = Math.cbrt(l);\n  const m_ = Math.cbrt(m);\n  const s_ = Math.cbrt(s);\n  const L = 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_;\n  const A = 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_;\n  const B = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_;\n  const C = Math.hypot(A, B);\n  let H = (Math.atan2(B, A) * 180) / Math.PI;\n  if (H < 0) H += 360;\n  return { L, C, H };\n}\n\n// oklch → rgb channels 0–255, clamped into sRGB gamut\nfunction oklchToRgb(L: number, C: number, H: number): { r: number; g: number; b: number } {\n  const hr = (H * Math.PI) / 180;\n  const a = C * Math.cos(hr);\n  const b = C * Math.sin(hr);\n  const l_ = L + 0.3963377774 * a + 0.2158037573 * b;\n  const m_ = L - 0.1055613458 * a - 0.0638541728 * b;\n  const s_ = L - 0.0894841775 * a - 1.291485548 * b;\n  const l = l_ ** 3;\n  const m = m_ ** 3;\n  const s = s_ ** 3;\n  const r = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;\n  const g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;\n  const bl = -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s;\n  const to = (c: number) => clamp(round(linearToSrgb(c) * 255), 0, 255);\n  return { r: to(r), g: to(g), b: to(bl) };\n}\n\nexport function hsvaToOklcha(hsva: Hsva): Oklcha {\n  const { r, g, b } = hsvaToRgba(hsva);\n  const { L, C, H } = rgbToOklch(r, g, b);\n  return { L, C, H, a: clamp01(hsva.a) };\n}\n\nexport function oklchaToHsva({ L, C, H, a }: Oklcha): Hsva {\n  const { r, g, b } = oklchToRgb(L, C, H);\n  return rgbaToHsva({ r, g, b, a });\n}\n\nconst hex2 = (n: number) => clamp(round(n), 0, 255).toString(16).padStart(2, \"0\");\n\n// 3/4/6/8 digits, leading # optional; null on malformed input\nexport function hexToRgba(input: string): Rgba | null {\n  const m = input.trim().replace(/^#/, \"\");\n  const read = (h: string) => Number.parseInt(h, 16);\n  if (m.length === 3 || m.length === 4) {\n    if (!/^[0-9a-f]+$/i.test(m)) return null;\n    const [red, green, blue, alpha] = m;\n    if (!red || !green || !blue) return null;\n    const r = read(red + red);\n    const g = read(green + green);\n    const b = read(blue + blue);\n    const a = alpha ? read(alpha + alpha) / 255 : 1;\n    return { r, g, b, a };\n  }\n  if (m.length === 6 || m.length === 8) {\n    if (!/^[0-9a-f]+$/i.test(m)) return null;\n    const r = read(m.slice(0, 2));\n    const g = read(m.slice(2, 4));\n    const b = read(m.slice(4, 6));\n    const a = m.length === 8 ? read(m.slice(6, 8)) / 255 : 1;\n    return { r, g, b, a };\n  }\n  return null;\n}\n\nexport function rgbaToHex(rgba: Rgba, withAlpha = rgba.a < 1): string {\n  const base = `#${hex2(rgba.r)}${hex2(rgba.g)}${hex2(rgba.b)}`;\n  return withAlpha ? `${base}${hex2(clamp01(rgba.a) * 255)}` : base;\n}\n\nconst trimNum = (n: number, d: number) => n.toFixed(d).replace(/\\.?0+$/, \"\");\n\n// near-neutral colors drop chroma+hue noise, matching shadcn v4 gray convention: `oklch(0.985 0 0)`\nfunction oklchTriplet(L: number, C: number, H: number): string {\n  const Ls = trimNum(L, 4);\n  if (C < 1e-4) return `${Ls} 0 0`;\n  return `${Ls} ${trimNum(C, 4)} ${trimNum(H, 3)}`;\n}\n\nexport function formatColor(hsva: Hsva, format: ColorFormat, opts?: { alpha?: boolean }): string {\n  const useAlpha = opts?.alpha !== false && hsva.a < 1;\n  const alphaSuffix = useAlpha ? ` / ${trimNum(clamp01(hsva.a), 3)}` : \"\";\n  switch (format) {\n    case \"hex\": {\n      return rgbaToHex(hsvaToRgba(hsva), useAlpha);\n    }\n    case \"rgb\": {\n      const { r, g, b } = hsvaToRgba(hsva);\n      return `rgb(${r} ${g} ${b}${alphaSuffix})`;\n    }\n    case \"hsl\": {\n      const { h, s, l } = hsvaToHsla(hsva);\n      return `hsl(${trimNum(wrapHue(h), 1)} ${trimNum(s, 1)}% ${trimNum(l, 1)}%${alphaSuffix})`;\n    }\n    case \"oklch\": {\n      const { L, C, H } = hsvaToOklcha(hsva);\n      return `oklch(${oklchTriplet(L, C, H)}${alphaSuffix})`;\n    }\n  }\n}\n\nconst NAMED: Record<string, string> = {\n  transparent: \"#00000000\",\n  black: \"#000000\",\n  white: \"#ffffff\",\n  red: \"#ff0000\",\n  green: \"#008000\",\n  blue: \"#0000ff\",\n  yellow: \"#ffff00\",\n  orange: \"#ffa500\",\n  purple: \"#800080\",\n  gray: \"#808080\",\n  grey: \"#808080\",\n};\n\n// CSS allows bare 0–1 number or percentage here\nfunction readAlpha(token: string | undefined): number {\n  if (token === undefined) return 1;\n  const t = token.trim();\n  if (t.endsWith(\"%\")) return clamp01(Number.parseFloat(t) / 100);\n  return clamp01(Number.parseFloat(t));\n}\n\nfunction parseHexColor(raw: string): Hsva | null {\n  const rgba = hexToRgba(raw);\n  return rgba ? rgbaToHsva(rgba) : null;\n}\n\nfunction parseRgbFunction(raw: string): Hsva | null {\n  const match = raw.match(/^rgba?\\(\\s*([\\d.]+%?)[\\s,]+([\\d.]+%?)[\\s,]+([\\d.]+%?)\\s*(?:[/,]\\s*([\\d.]+%?))?\\s*\\)$/);\n  if (!match) return null;\n  const [, red, green, blue, alpha] = match;\n  if (!red || !green || !blue) return null;\n  const chan = (t: string) => (t.endsWith(\"%\") ? (Number.parseFloat(t) / 100) * 255 : Number.parseFloat(t));\n  return rgbaToHsva({ r: chan(red), g: chan(green), b: chan(blue), a: readAlpha(alpha) });\n}\n\nfunction parseHslFunction(raw: string): Hsva | null {\n  const match = raw.match(/^hsla?\\(\\s*([\\d.]+)(?:deg)?[\\s,]+([\\d.]+)%[\\s,]+([\\d.]+)%\\s*(?:[/,]\\s*([\\d.]+%?))?\\s*\\)$/);\n  if (!match) return null;\n  const [, hue, saturation, lightness, alpha] = match;\n  if (!hue || !saturation || !lightness) return null;\n  return hslaToHsva({\n    h: Number.parseFloat(hue),\n    s: Number.parseFloat(saturation),\n    l: Number.parseFloat(lightness),\n    a: readAlpha(alpha),\n  });\n}\n\nfunction parseOklchFunction(raw: string): Hsva | null {\n  const match = raw.match(/^oklch\\(\\s*([\\d.]+%?)\\s+([\\d.]+%?)\\s+([\\d.]+)\\s*(?:[/,]\\s*([\\d.]+%?))?\\s*\\)$/);\n  if (!match) return null;\n  const [, lightness, chroma, hue, alpha] = match;\n  if (!lightness || !chroma || !hue) return null;\n  const L = lightness.endsWith(\"%\") ? Number.parseFloat(lightness) / 100 : Number.parseFloat(lightness);\n  // chroma as percentage is 0%→0, 100%→0.4 per CSS Color 4 reference range\n  const C = chroma.endsWith(\"%\") ? (Number.parseFloat(chroma) / 100) * 0.4 : Number.parseFloat(chroma);\n  return oklchaToHsva({ L, C, H: Number.parseFloat(hue), a: readAlpha(alpha) });\n}\n\n// Pure parser: identical server/client results keep controlled color UI hydration-safe.\nexport function parseColor(input: string): Hsva | null {\n  const raw = input.trim().toLowerCase();\n  if (!raw) return null;\n\n  const named = NAMED[raw];\n  if (named) return parseHexColor(named);\n  if (raw.startsWith(\"#\")) return parseHexColor(raw);\n  return parseRgbFunction(raw) ?? parseHslFunction(raw) ?? parseOklchFunction(raw);\n}\n\n// preserves hue (+sat at true black) on achromatic mutation so area/hue thumbs don't jump to red; `prev` = color before edit\nfunction preserveAchromatic(next: Hsva, prev: Hsva): Hsva {\n  const out = { ...next };\n  if (out.v === 0) {\n    out.h = prev.h;\n    out.s = prev.s;\n  } else if (out.s === 0) {\n    out.h = prev.h;\n  }\n  return out;\n}\n\n// channel set shown for given format. HEX borrows RGB channels (matching Figma).\nexport function getChannels(hsva: Hsva, format: ColorFormat): ChannelSpec[] {\n  const alpha: ChannelSpec = { id: \"a\", label: \"A\", value: round(clamp01(hsva.a) * 100), min: 0, max: 100, step: 1 };\n  switch (format) {\n    case \"hex\":\n    case \"rgb\": {\n      const { r, g, b } = hsvaToRgba(hsva);\n      return [\n        { id: \"r\", label: \"R\", value: round(r), min: 0, max: 255, step: 1 },\n        { id: \"g\", label: \"G\", value: round(g), min: 0, max: 255, step: 1 },\n        { id: \"b\", label: \"B\", value: round(b), min: 0, max: 255, step: 1 },\n        alpha,\n      ];\n    }\n    case \"hsl\": {\n      const { h, s, l } = hsvaToHsla(hsva);\n      return [\n        { id: \"h\", label: \"H\", value: round(wrapHue(h)), min: 0, max: 360, step: 1 },\n        { id: \"s\", label: \"S\", value: round(s), min: 0, max: 100, step: 1 },\n        { id: \"l\", label: \"L\", value: round(l), min: 0, max: 100, step: 1 },\n        alpha,\n      ];\n    }\n    case \"oklch\": {\n      const { L, C, H } = hsvaToOklcha(hsva);\n      return [\n        { id: \"okl\", label: \"L\", value: Number(trimNum(L, 3)), min: 0, max: 1, step: 0.001 },\n        { id: \"okc\", label: \"C\", value: Number(trimNum(C, 3)), min: 0, max: 0.4, step: 0.001 },\n        { id: \"okh\", label: \"H\", value: round(wrapHue(H)), min: 0, max: 360, step: 1 },\n        alpha,\n      ];\n    }\n  }\n}\n\n// channel `id` already encodes its color space (r/g/b, h/s/l, okl/okc/okh, a), no format arg needed\nexport function setChannel(hsva: Hsva, id: ChannelId, value: number): Hsva {\n  switch (id) {\n    case \"a\":\n      return { ...hsva, a: clamp01(value / 100) };\n    case \"r\":\n    case \"g\":\n    case \"b\": {\n      const rgba = hsvaToRgba(hsva);\n      const next = { ...rgba, [id]: clamp(value, 0, 255) };\n      return preserveAchromatic({ ...rgbaToHsva(next), a: hsva.a }, hsva);\n    }\n    case \"h\":\n      return { ...hsva, ...hslaToHsva({ ...hsvaToHsla(hsva), h: wrapHue(value) }), a: hsva.a };\n    case \"s\":\n    case \"l\": {\n      const hsla = hsvaToHsla(hsva);\n      const key = id === \"s\" ? \"s\" : \"l\";\n      const next = hslaToHsva({ ...hsla, [key]: clamp(value, 0, 100) });\n      return preserveAchromatic({ ...next, h: hsla.h, a: hsva.a }, hsva);\n    }\n    case \"okl\":\n    case \"okc\":\n    case \"okh\": {\n      const ok = hsvaToOklcha(hsva);\n      let mapped = ok;\n      if (id === \"okl\") mapped = { ...ok, L: clamp01(value) };\n      else if (id === \"okc\") mapped = { ...ok, C: clamp(value, 0, 0.5) };\n      else mapped = { ...ok, H: wrapHue(value) };\n      return preserveAchromatic({ ...oklchaToHsva(mapped), a: hsva.a }, hsva);\n    }\n  }\n}\n\n// origin top-left: x → saturation (0 left…100 right), y → value (100 top…0 bottom), clamped\nexport function pointToSaturationValue(\n  rect: { width: number; height: number },\n  offsetX: number,\n  offsetY: number,\n): { s: number; v: number } {\n  const s = rect.width <= 0 ? 0 : clamp01(offsetX / rect.width) * 100;\n  const v = rect.height <= 0 ? 0 : (1 - clamp01(offsetY / rect.height)) * 100;\n  return { s, v };\n}\n\nexport type GradientKind = \"linear\" | \"radial\" | \"conic\";\n\n// stops sorted by position; angle drives linear/conic direction, ignored for radial\nexport function formatGradient(stops: { position: number; color: string }[], type: GradientKind, angle: number): string {\n  const list = [...stops]\n    .sort((a, b) => a.position - b.position)\n    .map((stop) => `${stop.color} ${round(clamp01(stop.position) * 100)}%`)\n    .join(\", \");\n  switch (type) {\n    case \"linear\":\n      return `linear-gradient(${round(angle)}deg, ${list})`;\n    case \"radial\":\n      return `radial-gradient(circle, ${list})`;\n    case \"conic\":\n      return `conic-gradient(from ${round(angle)}deg, ${list})`;\n  }\n}\n\n// angle → hue, radius → saturation (clamped to ring); origin top-left, wheel inscribed in box\nexport function pointToHueSat(rect: { width: number; height: number }, offsetX: number, offsetY: number): { h: number; s: number } {\n  const cx = rect.width / 2;\n  const cy = rect.height / 2;\n  const dx = offsetX - cx;\n  const dy = offsetY - cy;\n  const radius = Math.min(cx, cy);\n  const dist = Math.hypot(dx, dy);\n  // atan2 with screen-y flipped so 0° points right and hue increases counter-clockwise → clockwise.\n  let h = (Math.atan2(dy, dx) * 180) / Math.PI;\n  h = wrapHue(h);\n  const s = radius <= 0 ? 0 : clamp01(dist / radius) * 100;\n  return { h, s };\n}\n"
    },
    {
      "path": "src/registry/lib/contrast.ts",
      "target": "@components/control-ui/lib/contrast.ts",
      "type": "registry:lib",
      "content": "// WCAG 2.x contrast for ColorPicker a11y advice; pure+SSR-safe\n// \"fix\" nudges OKLCH lightness (hue+chroma held, so identity survives, move stays perceptually uniform) until it clears target ratio vs background\n// adapted from theme-drawer's fixTextForeground; copied not imported (primitive installs by file-copy)\n\nimport { type Hsva, hsvaToOklcha, hsvaToRgba, oklchaToHsva, type Rgba } from \"./color\";\n\nexport type WcagLevel = \"AA\" | \"AAA\";\nexport type WcagLevels = { AA: boolean; AAA: boolean };\n\nexport const AA_RATIO = 4.5;\nexport const AAA_RATIO = 7;\nexport const TARGET_RATIO: Record<WcagLevel, number> = { AA: AA_RATIO, AAA: AAA_RATIO };\n\nfunction channelLuminance(c: number): number {\n  const s = c / 255;\n  return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;\n}\n\n// WCAG relative luminance — gamma-corrected sRGB.\nexport function luminanceFromRgb({ r, g, b }: { r: number; g: number; b: number }): number {\n  return 0.2126 * channelLuminance(r) + 0.7152 * channelLuminance(g) + 0.0722 * channelLuminance(b);\n}\n\nexport function contrastRatio(a: { r: number; g: number; b: number }, b: { r: number; g: number; b: number }): number {\n  const la = luminanceFromRgb(a) + 0.05;\n  const lb = luminanceFromRgb(b) + 0.05;\n  return la > lb ? la / lb : lb / la;\n}\n\nexport function wcagLevels(ratio: number): WcagLevels {\n  return { AA: ratio >= AA_RATIO, AAA: ratio >= AAA_RATIO };\n}\n\n// next unmet level fix should aim for: AA first, then AAA, then nothing.\nexport function nextFixLevel(levels: WcagLevels): WcagLevel | null {\n  if (!levels.AA) return \"AA\";\n  if (!levels.AAA) return \"AAA\";\n  return null;\n}\n\n// background alpha ignored — WCAG assumes opaque backdrop\nexport function contrastOf(color: Hsva, background: Rgba): number {\n  return contrastRatio(hsvaToRgba(color), background);\n}\n\n// scans both directions, returns SMALLEST passing move; if none reach target, returns extreme with best worst-case ratio; null if already passing\nexport function fixColorForContrast(color: Hsva, background: Rgba, target: number = AA_RATIO): Hsva | null {\n  const ok = hsvaToOklcha(color);\n  const ratioAtL = (L: number) => contrastRatio(hsvaToRgba(oklchaToHsva({ L, C: ok.C, H: ok.H, a: 1 })), background);\n  if (ratioAtL(ok.L) >= target) return null;\n\n  let best: { L: number; dist: number } | null = null;\n  for (let step = 0; step <= 100; step++) {\n    const L = step / 100;\n    if (ratioAtL(L) < target) continue;\n    const dist = Math.abs(L - ok.L);\n    if (!best || dist < best.dist) best = { L, dist };\n  }\n  const pick = (L: number) => oklchaToHsva({ L, C: ok.C, H: ok.H, a: color.a });\n  if (best) return pick(best.L);\n  // Nothing passed: fall back to whichever extreme maximizes ratio.\n  return ratioAtL(0) >= ratioAtL(1) ? pick(0) : pick(1);\n}\n"
    },
    {
      "path": "src/registry/sources/control-ui/recipes/color-picker-details.css",
      "target": "@components/control-ui/styles/recipes/color-picker-details.css",
      "type": "registry:file",
      "content": "@layer components {\n  :where([data-control-family=\"color-picker\"][data-slot=\"trigger-color\"]) {\n    border-radius: inherit;\n  }\n\n  :where([data-control-family=\"color-picker\"][data-slot=\"trigger\"]) {\n    border-radius: var(--cui-color-picker-trigger-radius);\n    outline-style: none;\n    transition-property:\n      color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events;\n    transition-timing-function: var(--ease-standard);\n    transition-duration: var(--duration-fast);\n    box-shadow: var(--cui-color-picker-trigger-shadow);\n  }\n\n  :where([data-control-family=\"color-picker\"][data-slot=\"trigger\"]:disabled) {\n    opacity: 50%;\n  }\n\n  :where([data-control-family=\"color-picker\"][data-slot=\"area\"][data-disabled=\"true\"]) {\n    opacity: 50%;\n  }\n\n  :where([data-control-family=\"color-picker\"][data-slot=\"wheel\"][data-disabled=\"true\"]) {\n    opacity: 50%;\n  }\n\n  :where([data-control-family=\"color-picker\"][data-slot=\"area-saturation\"]) {\n    background-image: linear-gradient(to right in srgb, var(--color-white), transparent);\n  }\n\n  :where([data-control-family=\"color-picker\"][data-slot=\"area-brightness\"]) {\n    background-image: linear-gradient(to top in srgb, var(--color-black), transparent);\n  }\n\n  :where([data-control-family=\"color-picker\"][data-slot=\"contrast-level\"][data-passing=\"true\"]) {\n    background-color: color-mix(in oklab, var(--foreground) 6%, transparent);\n    --_color-picker-contrast-level-foreground: var(--foreground);\n    color: var(--_color-picker-contrast-level-foreground);\n  }\n\n  :where([data-control-family=\"color-picker\"][data-slot=\"contrast-level\"]:not([data-passing=\"true\"])) {\n    --_color-picker-contrast-level-foreground: var(--muted-foreground);\n    color: var(--_color-picker-contrast-level-foreground);\n    text-decoration-line: line-through;\n    text-decoration-thickness: 1px;\n  }\n\n  :where([data-control-family=\"color-picker\"][data-slot=\"contrast-ratio\"]) {\n    font-variant-numeric: tabular-nums;\n  }\n\n  :where(\n    [data-control-family=\"color-picker\"][data-slot=\"wheel\"][data-dragging] [data-control-family=\"color-picker\"][data-slot=\"wheel-thumb\"]\n  ),\n  :where(\n    [data-control-family=\"color-picker\"][data-slot=\"area\"][data-dragging] [data-control-family=\"color-picker\"][data-slot=\"area-thumb\"]\n  ) {\n    scale: 110% 110%;\n  }\n}\n"
    },
    {
      "path": "src/registry/sources/control-ui/recipes/color-picker-values.css",
      "target": "@components/control-ui/styles/recipes/color-picker-values.css",
      "type": "registry:file",
      "content": "@layer components {\n  :where([data-control-family=\"color-picker\"][data-slot=\"output-value\"]) {\n    font-family: var(--font-mono);\n    font-size: var(--text-caption);\n    color: var(--muted-foreground);\n  }\n\n  :where([data-control-family=\"color-picker\"][data-slot=\"output-swatch\"]) {\n    border-radius: var(--cui-color-picker-output-swatch-radius);\n    box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--border) 60%, transparent);\n  }\n\n  :where([data-control-family=\"color-picker\"][data-slot=\"contrast-level\"]) {\n    border-radius: var(--radius-sm);\n    font-weight: var(--font-weight-medium);\n  }\n\n  :where([data-control-family=\"color-picker\"][data-slot=\"contrast-fix\"]) {\n    border-radius: var(--radius-sm);\n    font-weight: var(--font-weight-medium);\n    color: var(--foreground);\n    text-decoration-line: underline;\n    text-decoration-style: dotted;\n    text-underline-offset: 2px;\n  }\n\n  @media (hover: hover) {\n    :where([data-control-family=\"color-picker\"][data-slot=\"contrast-fix\"]:hover) {\n      background-color: color-mix(in oklab, var(--foreground) 6%, transparent);\n    }\n  }\n\n  :where([data-control-family=\"color-picker\"][data-slot=\"contrast-ratio\"]) {\n    color: var(--muted-foreground);\n  }\n\n  :where([data-control-family=\"color-picker\"][data-slot=\"contrast\"]) {\n    font-size: var(--text-caption);\n  }\n\n  :where([data-control-family=\"color-picker\"][data-slot=\"swatch-add\"]) {\n    border-radius: var(--radius-md);\n    border-width: 1px;\n    border-style: dashed;\n    --_color-picker-swatch-add-border-color: var(--border);\n    border-color: var(--_color-picker-swatch-add-border-color);\n    --_color-picker-swatch-add-foreground: var(--muted-foreground);\n    color: var(--_color-picker-swatch-add-foreground);\n    outline-style: none;\n    transition-property:\n      color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events;\n    transition-timing-function: var(--ease-standard);\n    transition-duration: var(--duration-fast);\n  }\n\n  @media (hover: hover) {\n    :where([data-control-family=\"color-picker\"][data-slot=\"swatch-add\"]:hover) {\n      --_color-picker-swatch-add-border-color: color-mix(in oklab, var(--foreground) 40%, transparent);\n      border-color: var(--_color-picker-swatch-add-border-color);\n      --_color-picker-swatch-add-foreground: var(--foreground);\n      color: var(--_color-picker-swatch-add-foreground);\n    }\n  }\n\n  :where([data-control-family=\"color-picker\"][data-slot=\"swatch\"]) {\n    border-radius: var(--cui-color-picker-swatch-radius);\n    outline-style: none;\n    transition-property:\n      color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events;\n    transition-timing-function: var(--ease-standard);\n    transition-duration: var(--duration-fast);\n    --_color-picker-swatch-shadow: inset 0 0 0 1px color-mix(in oklab, var(--border) 60%, transparent);\n    box-shadow: var(--_color-picker-swatch-shadow);\n  }\n\n  :where([data-control-family=\"color-picker\"][data-slot=\"swatch\"][data-selected=\"true\"]) {\n    --_color-picker-swatch-shadow: inset 0 0 0 2px var(--foreground);\n    box-shadow: var(--_color-picker-swatch-shadow);\n  }\n\n  :where([data-control-family=\"color-picker\"][data-slot=\"swatches-label\"]) {\n    font-size: var(--text-micro);\n    color: var(--muted-foreground);\n  }\n\n  :where([data-control-family=\"color-picker\"][data-slot=\"channel-label\"]) {\n    font-size: var(--text-micro);\n    color: var(--muted-foreground);\n  }\n\n  :where([data-control-family=\"color-picker\"][data-slot=\"channel\"]) {\n    text-align: center;\n  }\n\n  :where([data-control-family=\"color-picker\"][data-slot=\"input\"]) {\n    font-family: var(--font-mono);\n    font-size: var(--text-caption);\n  }\n}\n\n@property --cui-color-picker-output-swatch-radius {\n  syntax: \"<length-percentage>+\";\n  inherits: true;\n  initial-value: 0px;\n}\n\n@property --cui-color-picker-swatch-radius {\n  syntax: \"<length-percentage>+\";\n  inherits: true;\n  initial-value: 0px;\n}\n"
    },
    {
      "path": "src/registry/sources/control-ui/recipes/color-picker.css",
      "target": "@components/control-ui/styles/recipes/color-picker.css",
      "type": "registry:file",
      "content": "@layer components {\n  :where([data-control-family=\"color-picker\"][data-slot=\"trigger\"]),\n  :where([data-control-family=\"color-picker\"][data-slot=\"area\"]),\n  :where([data-control-family=\"color-picker\"][data-slot=\"hue\"]),\n  :where([data-control-family=\"color-picker\"][data-slot=\"alpha\"]),\n  :where([data-control-family=\"color-picker\"][data-slot=\"wheel\"]),\n  :where([data-control-family=\"color-picker\"][data-slot=\"swatch\"]),\n  :where([data-control-family=\"color-picker\"][data-slot=\"output\"]) {\n    --cui-color-picker-area-radius: var(--radius-field);\n    --cui-color-picker-output-swatch-radius: var(--radius-md);\n    --cui-color-picker-slider-thumb-background: transparent;\n    --cui-color-picker-slider-thumb-radius: calc(infinity * 1px);\n    --cui-color-picker-swatch-radius: var(--radius-md);\n    --cui-color-picker-trigger-radius: var(--radius-control);\n    --cui-color-picker-trigger-shadow: inset 0 0 0 1px var(--border);\n  }\n  :where([data-control-family=\"color-picker\"][data-slot=\"hue\"]),\n  :where([data-control-family=\"color-picker\"][data-slot=\"alpha\"]) {\n    --_color-picker-slider-thumb-outline: none;\n    --_color-picker-slider-thumb-scale: 1;\n    --_color-picker-slider-thumb-transition-property: scale;\n  }\n\n  :where([data-control-family=\"color-picker\"][data-slot=\"hue-thumb\"]),\n  :where([data-control-family=\"color-picker\"][data-slot=\"alpha-thumb\"]) {\n    border-radius: var(--cui-color-picker-slider-thumb-radius);\n    border-width: 2px;\n    border-style: solid;\n    border-color: var(--color-white);\n    background: var(--cui-color-picker-slider-thumb-background);\n    box-shadow: 0 0 0 1px oklch(from var(--foreground) l c h / 0.35);\n    outline-style: var(--_color-picker-slider-thumb-outline);\n    transition-property: var(--_color-picker-slider-thumb-transition-property);\n    transition-duration: var(--duration-fast);\n    transition-timing-function: var(--ease-standard);\n    scale: var(--_color-picker-slider-thumb-scale);\n  }\n\n  :where([data-control-family=\"color-picker\"][data-slot=\"hue-thumb\"][data-dragging]),\n  :where([data-control-family=\"color-picker\"][data-slot=\"alpha-thumb\"][data-dragging]) {\n    --_color-picker-slider-thumb-scale: 110% 110%;\n  }\n\n  :where([data-control-family=\"color-picker\"][data-slot=\"wheel-thumb\"]) {\n    border-radius: calc(infinity * 1px);\n    border-style: solid;\n    border-width: 2px;\n    border-color: var(--color-white);\n    box-shadow: var(--_color-picker-wheel-thumb-shadow);\n  }\n\n  :where([data-control-family=\"color-picker\"][data-slot=\"wheel\"]) {\n    border-radius: calc(infinity * 1px);\n    box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--border) 60%, transparent);\n    --_color-picker-wheel-thumb-shadow: 0 0 0 1px oklch(from var(--foreground) l c h / 0.4);\n  }\n\n  :where([data-control-family=\"color-picker\"][data-slot=\"alpha-track\"]) {\n    border-radius: calc(infinity * 1px);\n    box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--border) 50%, transparent);\n  }\n\n  :where([data-control-family=\"color-picker\"][data-slot=\"hue-track\"]) {\n    border-radius: calc(infinity * 1px);\n    box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--border) 50%, transparent);\n  }\n\n  :where([data-control-family=\"color-picker\"][data-slot=\"area-thumb\"]) {\n    border-radius: calc(infinity * 1px);\n    border-style: solid;\n    border-width: 2px;\n    border-color: var(--color-white);\n    transition-property: box-shadow;\n    transition-timing-function: var(--ease-standard);\n    transition-duration: var(--duration-fast);\n    box-shadow: var(--_color-picker-area-thumb-shadow);\n  }\n\n  :where([data-control-family=\"color-picker\"][data-slot=\"area\"]) {\n    border-radius: var(--cui-color-picker-area-radius);\n    box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--border) 60%, transparent);\n    --_color-picker-area-thumb-shadow: 0 0 0 1px oklch(from var(--foreground) l c h / 0.4);\n  }\n}\n\n@property --cui-color-picker-area-radius {\n  syntax: \"<length-percentage>+\";\n  inherits: true;\n  initial-value: 0px;\n}\n\n@property --cui-color-picker-trigger-radius {\n  syntax: \"<length-percentage>+\";\n  inherits: true;\n  initial-value: 0px;\n}\n\n@property --cui-color-picker-trigger-shadow {\n  syntax: \"*\";\n  inherits: true;\n}\n\n@property --cui-color-picker-slider-thumb-radius {\n  syntax: \"<length-percentage>\";\n  inherits: true;\n  initial-value: 0px;\n}\n\n@property --cui-color-picker-slider-thumb-background {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n"
    },
    {
      "path": "src/registry/sources/control-ui/recipes/popup-controls.css",
      "target": "@components/control-ui/styles/recipes/popup-controls.css",
      "type": "registry:file",
      "content": "@layer components {\n  :where([data-control-family=\"popup\"][data-popup-kind=\"alert-dialog\"][data-slot=\"content\"]) {\n    outline-style: none;\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"hover-card\"][data-slot=\"positioner\"]) {\n    outline-style: none;\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"navigation-menu\"][data-slot=\"link\"]) {\n    text-decoration-line: none;\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"menubar\"][data-slot=\"sub-trigger-indicator\"]) {\n    line-height: 1;\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"rich-tooltip\"][data-slot=\"content\"]) {\n    text-align: left;\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"rich-tooltip\"][data-slot=\"content\"][data-starting-style]) {\n    --_popup-rich-tooltip-content-opacity: 0%;\n    opacity: var(--_popup-rich-tooltip-content-opacity);\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"rich-tooltip\"][data-slot=\"content\"][data-ending-style]) {\n    --_popup-rich-tooltip-content-opacity: 0%;\n    opacity: var(--_popup-rich-tooltip-content-opacity);\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"rich-tooltip\"][data-slot=\"positioner\"]) {\n    outline-style: none;\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"dialog\"][data-slot=\"content\"]) {\n    outline-style: none;\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"color-picker\"][data-slot=\"positioner\"]) {\n    outline-style: none;\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"context-menu\"][data-slot=\"sub-trigger-indicator\"]) {\n    line-height: 1;\n  }\n\n  :where([data-control-family=\"button\"][data-slot=\"root\"][data-popup-part=\"close\"][data-icon-only=\"true\"]) {\n    --_popup-button-root-close-opacity: 70%;\n    opacity: var(--_popup-button-root-close-opacity);\n  }\n\n  @media (hover: hover) {\n    :where([data-control-family=\"button\"][data-slot=\"root\"][data-popup-part=\"close\"][data-icon-only=\"true\"]:hover) {\n      --_popup-button-root-close-opacity: 100%;\n      opacity: var(--_popup-button-root-close-opacity);\n    }\n  }\n}\n"
    },
    {
      "path": "src/registry/sources/control-ui/recipes/popup-lists.css",
      "target": "@components/control-ui/styles/recipes/popup-lists.css",
      "type": "registry:file",
      "content": "@layer components {\n  :where([data-control-family=\"popup\"][data-popup-kind=\"model-switcher\"][data-slot=\"hint\"]) {\n    font-size: var(--text-micro);\n    color: var(--muted-foreground);\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"trigger-menu\"][data-slot=\"group-label\"]) {\n    font-size: var(--text-micro);\n    font-weight: var(--font-weight-medium);\n    letter-spacing: 0.08em;\n    color: var(--muted-foreground);\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"trigger-menu\"][data-slot=\"empty\"]) {\n    font-size: var(--text-body);\n    color: var(--muted-foreground);\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"trigger-menu\"][data-slot=\"icon\"]) {\n    color: var(--muted-foreground);\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"trigger-menu\"][data-slot=\"positioner\"]) {\n    outline-style: none;\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"combobox\"][data-slot=\"group-label\"]) {\n    font-size: var(--text-caption);\n    font-weight: var(--font-weight-medium);\n    color: var(--muted-foreground);\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"combobox\"][data-slot=\"item-indicator\"]) {\n    color: var(--foreground);\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"combobox\"][data-slot=\"empty\"]) {\n    text-align: center;\n    font-size: var(--text-body);\n    color: var(--muted-foreground);\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"combobox\"][data-slot=\"positioner\"]) {\n    outline-style: none;\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"command\"][data-slot=\"group\"] [cmdk-group-heading]) {\n    font-size: var(--text-caption);\n    font-weight: var(--font-weight-medium);\n    letter-spacing: 0.08em;\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"command\"][data-slot=\"empty\"]) {\n    text-align: center;\n    font-size: var(--text-body);\n    color: var(--muted-foreground);\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"command\"][data-slot=\"input-wrapper\"]) {\n    border-block-end: 1px solid var(--cui-popup-separator-color);\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"command\"][data-slot=\"input\"]) {\n    background: transparent;\n    color: var(--foreground);\n    outline-style: none;\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"command\"][data-slot=\"input\"])::placeholder,\n  :where([data-control-family=\"popup\"][data-popup-kind=\"command\"] [cmdk-group-heading]) {\n    color: var(--muted-foreground);\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"command\"][data-slot=\"input\"]:disabled) {\n    opacity: 0.5;\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"command\"][data-slot=\"group\"]) {\n    color: var(--foreground);\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"command\"][data-slot=\"input-icon\"]) {\n    color: var(--muted-foreground);\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"command\"][data-slot=\"dialog-root\"]) {\n    transition-property: grid-template-rows;\n    transition-duration: var(--duration-slow);\n    transition-timing-function: var(--ease-emphasized);\n    transition-delay: var(--duration-fast);\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"autocomplete\"][data-slot=\"group-label\"]) {\n    font-size: var(--text-caption);\n    font-weight: var(--font-weight-medium);\n    color: var(--muted-foreground);\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"autocomplete\"][data-slot=\"empty\"]) {\n    text-align: center;\n    font-size: var(--text-body);\n    color: var(--muted-foreground);\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"autocomplete\"][data-slot=\"positioner\"]) {\n    outline-style: none;\n  }\n}\n"
    },
    {
      "path": "src/registry/sources/control-ui/recipes/popup-motion.css",
      "target": "@components/control-ui/styles/recipes/popup-motion.css",
      "type": "registry:file",
      "content": "@layer components {\n  :where([data-control-family=\"popup\"][data-popup-kind=\"sheet\"][data-popup-part=\"surface\"]) {\n    outline-style: none;\n    transition-property: translate;\n    transition-duration: var(--duration-base);\n    transition-timing-function: var(--ease-emphasized);\n    border-radius: 0;\n    border-style: solid;\n    border-width: 0px;\n  }\n\n  :where(\n    [data-control-family=\"popup\"][data-popup-kind=\"sheet\"][data-popup-part=\"surface\"][data-side=\"left\"]:is(\n      [data-starting-style],\n      [data-ending-style]\n    )\n  ) {\n    translate: -100% 0;\n  }\n\n  :where(\n    [data-control-family=\"popup\"][data-popup-kind=\"sheet\"][data-popup-part=\"surface\"][data-side=\"right\"]:is(\n      [data-starting-style],\n      [data-ending-style]\n    )\n  ) {\n    translate: 100% 0;\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"tooltip\"][data-slot=\"arrow-shape\"]) {\n    fill: currentcolor;\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"tooltip\"][data-slot=\"content\"]) {\n    font-size: var(--text-xs);\n    line-height: var(--text-xs--line-height);\n    text-wrap: balance;\n    outline-style: none;\n    --_popup-tooltip-content-transition-property: opacity, scale;\n    transition-property: var(--_popup-tooltip-content-transition-property);\n    transition-timing-function: var(--ease-standard);\n    transition-duration: var(--duration-fast);\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"tooltip\"][data-slot=\"content\"][data-instant]) {\n    --_popup-tooltip-content-transition-property: none;\n    transition-property: var(--_popup-tooltip-content-transition-property);\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"tooltip\"][data-slot=\"content\"][data-ending-style]) {\n    --_popup-tooltip-content-opacity: 0%;\n    opacity: var(--_popup-tooltip-content-opacity);\n    --_popup-tooltip-content-scale: 95% 95%;\n    scale: var(--_popup-tooltip-content-scale);\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"tooltip\"][data-slot=\"content\"][data-starting-style]) {\n    --_popup-tooltip-content-opacity: 0%;\n    opacity: var(--_popup-tooltip-content-opacity);\n    --_popup-tooltip-content-scale: 95% 95%;\n    scale: var(--_popup-tooltip-content-scale);\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"tooltip\"][data-slot=\"positioner\"]) {\n    outline-style: none;\n  }\n\n  :where(\n    [data-control-family=\"popup\"][data-popup-kind=\"tooltip\"][data-slot=\"content\"][data-popup-part=\"surface\"]:not([data-arrow=\"true\"])\n  ) {\n    --_popup-tooltip-content-radius: var(--cui-popup-radius);\n    border-radius: var(--_popup-tooltip-content-radius);\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"tooltip\"][data-slot=\"content\"][data-popup-part=\"surface\"][data-arrow=\"true\"]) {\n    --_popup-tooltip-content-radius: clamp(0px, var(--cui-popup-radius), 3px);\n    border-radius: var(--_popup-tooltip-content-radius);\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"tooltip\"][data-slot=\"arrow\"][data-side=\"top\"]) {\n    --_popup-tooltip-arrow-rotate: 180deg;\n    rotate: var(--_popup-tooltip-arrow-rotate);\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"tooltip\"][data-slot=\"arrow\"][data-side=\"left\"]) {\n    --_popup-tooltip-arrow-rotate: 90deg;\n    rotate: var(--_popup-tooltip-arrow-rotate);\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"tooltip\"][data-slot=\"arrow\"][data-side=\"right\"]) {\n    --_popup-tooltip-arrow-rotate: calc(90deg * -1);\n    rotate: var(--_popup-tooltip-arrow-rotate);\n  }\n}\n"
    },
    {
      "path": "src/registry/sources/control-ui/recipes/popup.css",
      "target": "@components/control-ui/styles/recipes/popup.css",
      "type": "registry:file",
      "content": "@layer components {\n  :where([data-control-family=\"popup\"][data-popup-part=\"surface\"]),\n  :where([data-control-family=\"popup\"][data-popup-part=\"list-surface\"]),\n  :where([data-control-family=\"popup\"][data-popup-part=\"bar\"]) {\n    --cui-popup-radius: var(--radius-popover);\n    --cui-popup-background: var(--popover);\n    --cui-popup-foreground: var(--popover-foreground);\n    --cui-popup-border-color: var(--border);\n    --cui-popup-border-width: 1px;\n    --cui-popup-shadow: var(--shadow-pop);\n    --cui-popup-backdrop-filter: blur(var(--backdrop-blur-popover));\n    --cui-popup-item-radius: var(--radius-popup-item);\n    --cui-popup-item-foreground: var(--foreground);\n    --cui-popup-item-highlight-background: oklch(from var(--foreground) l c h / 0.06);\n    --cui-popup-item-highlight-foreground: var(--cui-popup-item-foreground);\n    --cui-popup-item-highlight-muted-foreground: var(--muted-foreground);\n    --cui-popup-item-disabled-opacity: 0.4;\n    --cui-popup-separator-color: var(--border);\n    --cui-popup-shortcut-foreground: var(--muted-foreground);\n    --_popup-transform-origin: var(--transform-origin);\n    border-radius: var(--cui-popup-radius);\n    background-color: var(--cui-popup-background);\n    box-shadow:\n      inset 0 0 0 var(--cui-popup-border-width) var(--cui-popup-border-color),\n      var(--cui-popup-shadow);\n    color: var(--cui-popup-foreground);\n    outline: none;\n    -webkit-backdrop-filter: var(--cui-popup-backdrop-filter, none);\n    backdrop-filter: var(--cui-popup-backdrop-filter, none);\n    transform-origin: var(--_popup-transform-origin);\n  }\n  :where([data-control-family=\"popup\"][data-popup-part=\"surface\"]:not([data-popup-static]):not([data-slot=\"viewport\"])),\n  :where([data-control-family=\"popup\"][data-popup-part=\"list-surface\"]) {\n    transition-property: opacity, scale;\n    transition-duration: var(--duration-base);\n    transition-timing-function: var(--ease-emphasized);\n    transition-behavior: allow-discrete;\n  }\n  :where(\n    [data-control-family=\"popup\"][data-popup-part=\"surface\"]:not(\n      :is([data-popup-kind=\"sheet\"], [data-popup-kind=\"drawer\"], [data-popup-kind=\"toast\"])\n    ):not([data-slot=\"viewport\"]):where([data-starting-style], [data-ending-style])\n  ),\n  :where([data-control-family=\"popup\"][data-popup-part=\"list-surface\"]:where([data-starting-style], [data-ending-style])) {\n    scale: 0.95;\n    opacity: 0;\n  }\n  @starting-style {\n    :where(\n        [data-control-family=\"popup\"][data-popup-part=\"surface\"]:not(\n          :is([data-popup-kind=\"sheet\"], [data-popup-kind=\"drawer\"], [data-popup-kind=\"toast\"])\n        ):not([data-slot=\"viewport\"])\n      ),\n    :where([data-control-family=\"popup\"][data-popup-part=\"list-surface\"]) {\n      scale: 0.95;\n      opacity: 0;\n    }\n  }\n  :where([data-control-family=\"popup\"][data-popup-part=\"backdrop\"]) {\n    --cui-popup-backdrop-background: oklch(from var(--foreground) l c h / var(--overlay-opacity));\n    background-color: var(--cui-popup-backdrop-background);\n    -webkit-backdrop-filter: blur(var(--backdrop-blur-overlay));\n    backdrop-filter: blur(var(--backdrop-blur-overlay));\n    transition: opacity var(--duration-base) var(--ease-standard);\n  }\n  :where(.dark [data-control-family=\"popup\"][data-popup-part=\"backdrop\"]) {\n    --cui-popup-backdrop-background: oklch(from var(--background) l c h / var(--overlay-opacity));\n  }\n  :where([data-control-family=\"popup\"][data-popup-part=\"backdrop\"]:is([data-starting-style], [data-ending-style])) {\n    opacity: 0;\n  }\n  :where([data-control-family=\"popup\"][data-popup-part=\"list-surface\"]) {\n    padding: var(--popover-padding);\n  }\n  :where([data-control-family=\"popup\"][data-popup-part=\"bar\"]) {\n    --cui-popup-radius: var(--radius-control);\n    --cui-popup-background: var(--card);\n    --cui-popup-shadow: var(--shadow-xs);\n  }\n  :where([data-control-family=\"popup\"][data-popup-part=\"surface\"][data-surface=\"modal\"]) {\n    --cui-popup-radius: var(--radius-panel);\n    --cui-popup-shadow: var(--shadow-modal);\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"tooltip\"][data-popup-part=\"surface\"]) {\n    --cui-popup-radius: var(--radius-control);\n    --cui-popup-background: var(--foreground);\n    --cui-popup-foreground: var(--background);\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"sheet\"][data-popup-part=\"surface\"]) {\n    --cui-popup-radius: 0px;\n    --cui-popup-shadow: var(--shadow-modal);\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"dialog\"][data-slot=\"title\"]),\n  :where([data-control-family=\"popup\"][data-popup-kind=\"alert-dialog\"][data-slot=\"title\"]) {\n    font-size: var(--text-heading-3);\n    font-weight: 600;\n    line-height: 1;\n    letter-spacing: -0.025em;\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"dialog\"][data-slot=\"description\"]),\n  :where([data-control-family=\"popup\"][data-popup-kind=\"alert-dialog\"][data-slot=\"description\"]) {\n    color: var(--muted-foreground);\n    font-size: var(--text-body);\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"sheet\"][data-slot=\"title\"]) {\n    font-weight: 600;\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"sheet\"][data-slot=\"description\"]) {\n    color: var(--muted-foreground);\n    font-size: var(--text-body);\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"tooltip\"][data-slot=\"arrow\"]) {\n    color: var(--cui-popup-background);\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-part=\"item\"]),\n  :where([data-control-family=\"popup\"][data-popup-part=\"navigation-link\"]) {\n    cursor: pointer;\n    border-radius: var(--cui-popup-item-radius);\n    color: var(--cui-popup-item-foreground);\n    font-size: var(--text-body);\n    outline: none;\n    padding-block: 0.25rem;\n    padding-inline: calc(var(--padding-x) * 0.5);\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-part=\"navigation-link\"]) {\n    font-size: var(--text-body);\n    transition-property: color, background-color, box-shadow, translate;\n    transition-duration: var(--duration-fast);\n    transition-timing-function: var(--ease-standard);\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-part=\"navigation-link\"][data-variant=\"compact\"]) {\n    color: var(--muted-foreground);\n    font-size: var(--text-label);\n    font-weight: 500;\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-part=\"navigation-link\"]:where(:hover, :focus-visible, [data-active])) {\n    background-color: var(--cui-popup-item-highlight-background);\n    color: var(--foreground);\n  }\n\n  :where(\n    [data-control-family=\"popup\"][data-popup-part=\"item\"]:where(\n      [data-highlighted],\n      [data-selected]:not([data-selected=\"false\"]),\n      [data-popup-open]\n    )\n  ) {\n    --cui-popup-shortcut-foreground: var(--cui-popup-item-highlight-foreground);\n    --muted-foreground: var(--cui-popup-item-highlight-muted-foreground);\n    background-color: var(--cui-popup-item-highlight-background);\n    color: var(--cui-popup-item-highlight-foreground);\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-part=\"item\"]:is([data-disabled]:not([data-disabled=\"false\"]), [data-disabled=\"true\"])) {\n    cursor: not-allowed;\n    opacity: var(--cui-popup-item-disabled-opacity);\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-part=\"label\"]) {\n    color: var(--muted-foreground);\n    font-size: var(--text-micro);\n    font-weight: 500;\n    letter-spacing: 0.08em;\n    text-transform: uppercase;\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-part=\"separator\"]) {\n    background-color: var(--cui-popup-separator-color);\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-part=\"shortcut\"]) {\n    --_popup-shortcut-font-size: var(--text-body);\n    color: var(--cui-popup-shortcut-foreground);\n    font-size: var(--_popup-shortcut-font-size);\n    letter-spacing: 0.02em;\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"command\"][data-slot=\"shortcut\"]) {\n    --_popup-shortcut-font-size: var(--text-label);\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"select\"][data-slot=\"icon\"]),\n  :where([data-control-family=\"popup\"][data-popup-kind=\"navigation-menu\"][data-slot=\"icon\"]) {\n    color: var(--muted-foreground);\n    transition-property: rotate;\n    transition-duration: var(--duration-base);\n    transition-timing-function: var(--ease-emphasized);\n  }\n\n  :where(\n    [data-control-family=\"button\"][data-popup-kind=\"select\"][data-slot=\"trigger\"][data-popup-open]\n      [data-control-family=\"popup\"][data-popup-kind=\"select\"][data-slot=\"icon\"]\n  ),\n  :where(\n    [data-control-family=\"button\"][data-popup-kind=\"navigation-menu\"][data-slot=\"trigger\"][data-popup-open]\n      :where([data-control-family=\"popup\"][data-popup-kind=\"navigation-menu\"][data-slot=\"icon\"])\n  ) {\n    rotate: 180deg;\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"select\"][data-slot=\"item-indicator\"]) {\n    color: inherit;\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"context-menu\"][data-slot=\"radio-indicator\"]) {\n    border-radius: 9999px;\n    background-color: currentColor;\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"popover\"][data-slot=\"title\"]) {\n    font-size: var(--text-body);\n    font-weight: 600;\n    line-height: 1;\n    letter-spacing: -0.01em;\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"popover\"][data-slot=\"description\"]) {\n    color: var(--muted-foreground);\n    font-size: var(--text-body);\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"navigation-menu\"][data-slot=\"positioner\"]) {\n    transition-property: inset-block-start, inset-inline-start, inset-block-end, inset-inline-end;\n    transition-duration: var(--duration-base);\n    transition-timing-function: var(--ease-emphasized);\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"navigation-menu\"][data-slot=\"popup\"]) {\n    outline: none;\n    --_popup-transform-origin: var(--transform-origin);\n    transform-origin: var(--_popup-transform-origin);\n    transition-property: opacity, inline-size, block-size, scale;\n    transition-duration: var(--duration-base);\n    transition-timing-function: var(--ease-emphasized);\n  }\n\n  :where(\n    [data-control-family=\"popup\"][data-popup-kind=\"navigation-menu\"][data-slot=\"popup\"]:is([data-starting-style], [data-ending-style])\n  ) {\n    scale: 0.95;\n    opacity: 0;\n  }\n\n  :where([data-control-family=\"popup\"][data-popup-kind=\"navigation-menu\"][data-slot=\"content\"]) {\n    transition-property: opacity, translate;\n    transition-duration: var(--duration-base);\n    transition-timing-function: var(--ease-emphasized);\n  }\n\n  :where(\n    [data-control-family=\"popup\"][data-popup-kind=\"navigation-menu\"][data-slot=\"content\"]:is([data-starting-style], [data-ending-style])\n  ) {\n    opacity: 0;\n  }\n\n  :where(\n    [data-control-family=\"popup\"][data-popup-kind=\"navigation-menu\"][data-slot=\"content\"][data-starting-style][data-activation-direction=\"left\"]\n  ),\n  :where(\n    [data-control-family=\"popup\"][data-popup-kind=\"navigation-menu\"][data-slot=\"content\"][data-ending-style][data-activation-direction=\"right\"]\n  ) {\n    translate: -0.75rem 0;\n  }\n\n  :where(\n    [data-control-family=\"popup\"][data-popup-kind=\"navigation-menu\"][data-slot=\"content\"][data-starting-style][data-activation-direction=\"right\"]\n  ),\n  :where(\n    [data-control-family=\"popup\"][data-popup-kind=\"navigation-menu\"][data-slot=\"content\"][data-ending-style][data-activation-direction=\"left\"]\n  ) {\n    translate: 0.75rem 0;\n  }\n}\n\n@property --cui-popup-radius {\n  syntax: \"*\";\n  inherits: true;\n}\n\n@property --cui-popup-background {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n\n@property --cui-popup-foreground {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n\n@property --cui-popup-border-color {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n\n@property --cui-popup-border-width {\n  syntax: \"<length>\";\n  inherits: true;\n  initial-value: 0px;\n}\n\n@property --cui-popup-shadow {\n  syntax: \"*\";\n  inherits: true;\n}\n\n@property --cui-popup-backdrop-filter {\n  syntax: \"*\";\n  inherits: true;\n}\n\n@property --cui-popup-backdrop-background {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n\n@property --cui-popup-item-radius {\n  syntax: \"<length-percentage>\";\n  inherits: true;\n  initial-value: 0px;\n}\n\n@property --cui-popup-item-foreground {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n\n@property --cui-popup-item-highlight-background {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n\n@property --cui-popup-item-highlight-foreground {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n\n@property --cui-popup-item-highlight-muted-foreground {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n\n@property --cui-popup-item-disabled-opacity {\n  syntax: \"<number>\";\n  inherits: true;\n  initial-value: 1;\n}\n\n@property --cui-popup-separator-color {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n\n@property --cui-popup-shortcut-foreground {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n"
    },
    {
      "path": "src/registry/sources/control-ui/ui/color-picker.tsx",
      "target": "@components/control-ui/ui/color-picker.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { Popover as PopoverPrimitive } from \"@base-ui/react/popover\";\nimport { Slider as SliderPrimitive } from \"@base-ui/react/slider\";\nimport type { ComponentProps, CSSProperties, KeyboardEvent as ReactKeyboardEvent, ReactNode } from \"react\";\nimport { createContext, useContext, useEffect, useRef, useState, useSyncExternalStore } from \"react\";\nimport type { OpenChangeEventDetails } from \"@/components/control-ui/control-props\";\nimport type { ControlSize } from \"@/components/control-ui/control-variants\";\nimport { useColorArea } from \"@/components/control-ui/hooks/use-color-area\";\nimport type { ButtonKnobStyle } from \"@/components/control-ui/knob-contracts/button-knobs\";\nimport type { ColorPickerKnobStyle } from \"@/components/control-ui/knob-contracts/color-picker-knobs\";\nimport type { PopupKnobStyle } from \"@/components/control-ui/knob-contracts/popup-knobs\";\nimport { cn } from \"@/components/control-ui/lib/cn\";\nimport {\n  type ChannelId,\n  formatColor,\n  getChannels,\n  type Hsva,\n  hsvaToRgba,\n  parseColor,\n  pointToHueSat,\n  pointToSaturationValue,\n  setChannel,\n} from \"@/components/control-ui/lib/color\";\nimport { contrastOf, fixColorForContrast, nextFixLevel, TARGET_RATIO, wcagLevels } from \"@/components/control-ui/lib/contrast\";\nimport { skinEffects, skinId } from \"@/components/control-ui/skin\";\nimport { Button } from \"@/components/control-ui/ui/button\";\nimport { Input } from \"@/components/control-ui/ui/input\";\nimport { NumberField, NumberFieldGroup, NumberFieldInput } from \"@/components/control-ui/ui/number-field\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/control-ui/ui/select\";\n\nexport type ColorFormat = \"hex\" | \"rgb\" | \"hsl\" | \"oklch\";\n\nexport type ColorPickerProps = {\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  format?: ColorFormat;\n  defaultFormat?: ColorFormat;\n  onFormatChange?: (format: ColorFormat) => void;\n  alpha?: boolean;\n  disabled?: boolean;\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean, eventDetails: OpenChangeEventDetails) => void;\n  children?: ReactNode;\n};\n\nexport type ColorPickerTriggerProps = Omit<ComponentProps<\"button\">, \"style\"> & { style?: CSSProperties & ColorPickerKnobStyle };\n\nexport type ColorPickerContentProps = Omit<ComponentProps<\"div\">, \"style\"> & { style?: CSSProperties & PopupKnobStyle } & {\n  side?: \"top\" | \"right\" | \"bottom\" | \"left\";\n  align?: \"start\" | \"center\" | \"end\";\n  sideOffset?: number;\n};\n\nexport type ColorPickerPanelProps = Omit<ComponentProps<\"div\">, \"style\"> & { style?: CSSProperties & PopupKnobStyle };\n\nexport type ColorPickerAreaProps = Omit<ComponentProps<\"div\">, \"style\"> & { style?: CSSProperties & ColorPickerKnobStyle };\n\nexport type ColorPickerAreaThumbProps = Omit<ComponentProps<\"div\">, \"style\"> & { style?: CSSProperties & ColorPickerKnobStyle };\n\nexport type ColorPickerHueProps = Omit<Pick<ComponentProps<\"div\">, \"className\" | \"aria-label\" | \"aria-labelledby\" | \"style\">, \"style\"> & {\n  style?: CSSProperties & ColorPickerKnobStyle;\n};\n\nexport type ColorPickerAlphaProps = Omit<Pick<ComponentProps<\"div\">, \"className\" | \"aria-label\" | \"aria-labelledby\" | \"style\">, \"style\"> & {\n  style?: CSSProperties & ColorPickerKnobStyle;\n};\n\nexport type ColorPickerWheelProps = Omit<ComponentProps<\"div\">, \"style\"> & { style?: CSSProperties & ColorPickerKnobStyle };\n\nexport type ColorPickerEyeDropperProps = Omit<Omit<ComponentProps<\"button\">, \"onError\">, \"style\"> & {\n  style?: CSSProperties & ButtonKnobStyle;\n};\n\nexport type ColorPickerInputProps = Omit<Omit<ComponentProps<\"input\">, \"value\" | \"defaultValue\" | \"onChange\" | \"size\">, \"style\"> & {\n  style?: CSSProperties & ColorPickerKnobStyle;\n} & {\n  size?: ControlSize;\n};\n\nexport type ColorPickerFormatSelectProps = {\n  formats?: ColorFormat[];\n  size?: ControlSize;\n  className?: string;\n  \"aria-label\"?: string;\n  \"aria-labelledby\"?: string;\n  style?: CSSProperties & ButtonKnobStyle;\n};\n\nexport type ColorPickerChannelsProps = ComponentProps<\"div\"> & { style?: CSSProperties & ColorPickerKnobStyle };\n\nexport type ColorPickerChannelProps = Omit<Omit<ComponentProps<\"div\">, \"children\" | \"aria-label\">, \"style\"> & {\n  style?: CSSProperties & ColorPickerKnobStyle;\n} & {\n  channel: \"r\" | \"g\" | \"b\" | \"h\" | \"s\" | \"l\" | \"okl\" | \"okc\" | \"okh\" | \"a\";\n  label?: ReactNode;\n  \"aria-label\"?: string;\n};\n\nexport type ColorPickerSwatchesProps = ComponentProps<\"div\"> & {\n  colors?: string[];\n  label?: ReactNode;\n} & { style?: CSSProperties & ColorPickerKnobStyle };\n\nexport type ColorPickerSwatchProps = Omit<Omit<ComponentProps<\"button\">, \"color\">, \"style\"> & {\n  style?: CSSProperties & ColorPickerKnobStyle;\n} & {\n  color: string;\n};\n\nexport type ColorPickerSwatchAddProps = Omit<Omit<ComponentProps<\"button\">, \"onClick\">, \"style\"> & {\n  style?: CSSProperties & ColorPickerKnobStyle;\n} & {\n  onAdd?: (value: string) => void;\n};\n\nexport type ColorPickerContrastProps = Omit<Omit<ComponentProps<\"div\">, \"children\">, \"style\"> & {\n  style?: CSSProperties & ColorPickerKnobStyle;\n} & {\n  background?: string;\n};\n\nexport type ColorPickerOutputProps = Omit<Omit<ComponentProps<\"div\">, \"children\">, \"style\"> & {\n  style?: CSSProperties & ColorPickerKnobStyle;\n} & {\n  children?: ReactNode;\n  renderValue?: (state: { value: string }) => ReactNode;\n};\n\n// gradient data, deliberately not tokens\nconst HUE_GRADIENT =\n  \"linear-gradient(to right, hsl(0 100% 50%), hsl(60 100% 50%), hsl(120 100% 50%), hsl(180 100% 50%), hsl(240 100% 50%), hsl(300 100% 50%), hsl(360 100% 50%))\";\n// adapts to light and dark through border token\nconst CHECKER = \"repeating-conic-gradient(oklch(from var(--border) l c h) 0 25%, transparent 0 50%)\";\nconst CHECKER_SIZE = \"10px 10px\";\nconst RING_THUMB = \"block size-4 -translate-x-1/2\";\n\nconst isColorFormat = (v: string): v is ColorFormat => v === \"hex\" || v === \"rgb\" || v === \"hsl\" || v === \"oklch\";\nconst CHANNEL_NAMES: Record<ChannelId, string> = {\n  r: \"Red\",\n  g: \"Green\",\n  b: \"Blue\",\n  h: \"Hue\",\n  s: \"Saturation\",\n  l: \"Lightness\",\n  okl: \"OKLCH lightness\",\n  okc: \"OKLCH chroma\",\n  okh: \"OKLCH hue\",\n  a: \"Opacity\",\n};\n\n// Keep hue (and sat at true black) when color arrives achromatic, so thumbs don't jump to red.\nfunction reconcileAchromatic(prev: Hsva, next: Hsva): Hsva {\n  if (next.v === 0) return { ...next, h: prev.h, s: prev.s };\n  if (next.s === 0) return { ...next, h: prev.h };\n  return next;\n}\n\nfunction subscribeEyeDropper() {\n  return () => {};\n}\n\nfunction getEyeDropperSnapshot() {\n  return typeof window !== \"undefined\" && Boolean(window.EyeDropper);\n}\n\nfunction getEyeDropperServerSnapshot() {\n  return false;\n}\n\ntype ColorPickerContextValue = {\n  hsva: Hsva;\n  format: ColorFormat;\n  alpha: boolean;\n  disabled: boolean;\n  valueString: string;\n  setHsva: (partial: Partial<Hsva>) => void;\n  setChannelValue: (id: ChannelId, value: number) => void;\n  setFromString: (raw: string) => void;\n  setFormat: (format: ColorFormat) => void;\n};\n\nconst ColorPickerContext = createContext<ColorPickerContextValue | null>(null);\n\nfunction useColorPicker(): ColorPickerContextValue {\n  const ctx = useContext(ColorPickerContext);\n  if (!ctx) throw new Error(\"ColorPicker parts must be rendered inside <ColorPicker>.\");\n  return ctx;\n}\n\nfunction useColorState(props: ColorPickerProps): ColorPickerContextValue {\n  const {\n    value,\n    defaultValue = \"#000000\",\n    onValueChange,\n    format: formatProp,\n    defaultFormat = \"hex\",\n    onFormatChange,\n    alpha = true,\n    disabled = false,\n  } = props;\n\n  const [hsva, setHsvaState] = useState<Hsva>(() => parseColor(value ?? defaultValue) ?? { h: 0, s: 0, v: 0, a: 1 });\n  const [formatState, setFormatState] = useState<ColorFormat>(defaultFormat);\n  const format = formatProp ?? formatState;\n\n  // refs so setters read fresh state during fast pointer drags, without side effects inside state updater\n  const hsvaRef = useRef(hsva);\n  const formatRef = useRef(format);\n  const alphaRef = useRef(alpha);\n  const onValueChangeRef = useRef(onValueChange);\n  const lastEmitted = useRef<string | null>(null);\n\n  useEffect(() => {\n    hsvaRef.current = hsva;\n    formatRef.current = format;\n    alphaRef.current = alpha;\n    onValueChangeRef.current = onValueChange;\n  }, [hsva, format, alpha, onValueChange]);\n\n  // context value rebuilds every render anyway, so memoized handler identity buys nothing\n  const emit = (next: Hsva, fmt: ColorFormat) => {\n    const str = formatColor(next, fmt, { alpha: alphaRef.current });\n    lastEmitted.current = str;\n    onValueChangeRef.current?.(str);\n  };\n  const commit = (next: Hsva) => {\n    hsvaRef.current = next;\n    setHsvaState(next);\n    emit(next, formatRef.current);\n  };\n\n  // skips echo of its own emit, which would jitter thumbs\n  useEffect(() => {\n    if (lastEmitted.current === null) lastEmitted.current = formatColor(hsvaRef.current, formatRef.current, { alpha: alphaRef.current });\n    if (value === undefined || value === lastEmitted.current) return;\n    const parsed = parseColor(value);\n    if (!parsed) return;\n    const reconciled = reconcileAchromatic(hsvaRef.current, parsed);\n    hsvaRef.current = reconciled;\n    lastEmitted.current = value;\n    setHsvaState(reconciled);\n  }, [value]);\n\n  const setHsva = (partial: Partial<Hsva>) => commit({ ...hsvaRef.current, ...partial });\n  const setChannelValue = (id: ChannelId, next: number) => commit(setChannel(hsvaRef.current, id, next));\n  const setFromString = (raw: string) => {\n    const parsed = parseColor(raw);\n    if (!parsed) return;\n    commit(reconcileAchromatic(hsvaRef.current, parsed));\n  };\n  const setFormat = (next: ColorFormat) => {\n    if (formatProp === undefined) setFormatState(next);\n    formatRef.current = next;\n    onFormatChange?.(next);\n    emit(hsvaRef.current, next);\n  };\n\n  return {\n    hsva,\n    format,\n    alpha,\n    disabled,\n    valueString: formatColor(hsva, format, { alpha }),\n    setHsva,\n    setChannelValue,\n    setFromString,\n    setFormat,\n  };\n}\n\nexport function ColorPicker({ open, defaultOpen, onOpenChange, children, ...state }: ColorPickerProps) {\n  const ctx = useColorState({ ...state, children });\n  return (\n    <ColorPickerContext.Provider value={ctx}>\n      <PopoverPrimitive.Root open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange}>\n        {children}\n      </PopoverPrimitive.Root>\n    </ColorPickerContext.Provider>\n  );\n}\n\nexport function ColorPickerTrigger({\n  className,\n  \"aria-label\": ariaLabel,\n  \"aria-labelledby\": ariaLabelledBy,\n  ...props\n}: ColorPickerTriggerProps) {\n  const { valueString, disabled } = useColorPicker();\n  return (\n    <PopoverPrimitive.Trigger\n      data-control-ui=\"color-picker\"\n      data-control-family=\"color-picker\"\n      data-slot=\"trigger\"\n      data-disabled={disabled ? \"true\" : undefined}\n      disabled={disabled}\n      aria-label={ariaLabelledBy === undefined ? (ariaLabel ?? `Choose color (${valueString})`) : ariaLabel}\n      aria-labelledby={ariaLabelledBy}\n      className={cn(\"relative inline-flex size-8 shrink-0 cursor-pointer overflow-hidden disabled:cursor-not-allowed\", className)}\n      {...props}\n    >\n      <span\n        data-control-ui=\"color-picker\"\n        data-control-family=\"color-picker\"\n        data-slot=\"trigger-checker\"\n        aria-hidden\n        className=\"absolute inset-0\"\n        style={{ backgroundImage: CHECKER, backgroundSize: CHECKER_SIZE }}\n      />\n      <span\n        data-control-ui=\"color-picker\"\n        data-control-family=\"color-picker\"\n        data-slot=\"trigger-color\"\n        aria-hidden\n        className=\"absolute inset-0\"\n        style={{ backgroundColor: valueString }}\n      />\n    </PopoverPrimitive.Trigger>\n  );\n}\n\nexport function ColorPickerContent({\n  className,\n  children,\n  side = \"bottom\",\n  align = \"center\",\n  sideOffset = 6,\n  ...props\n}: ColorPickerContentProps) {\n  return (\n    <PopoverPrimitive.Portal>\n      {/* portal lands outside token-scoped tree, so scope is re-asserted here */}\n      <PopoverPrimitive.Positioner\n        data-control-ui=\"color-picker\"\n        data-popup-kind=\"color-picker\"\n        data-control-family=\"popup\"\n        data-slot=\"positioner\"\n        data-skin={skinId()}\n        data-effects={skinEffects()}\n        side={side}\n        align={align}\n        sideOffset={sideOffset}\n        className=\"z-[80]\"\n      >\n        <PopoverPrimitive.Popup\n          data-control-ui=\"color-picker\"\n          data-popup-kind=\"color-picker\"\n          data-control-family=\"popup\"\n          data-slot=\"content\"\n          data-surface=\"floating\"\n          data-popup-part=\"surface\"\n          className={cn(\"grid w-64 gap-3 p-3\", className)}\n          {...props}\n        >\n          {children}\n        </PopoverPrimitive.Popup>\n      </PopoverPrimitive.Positioner>\n    </PopoverPrimitive.Portal>\n  );\n}\n\nexport function ColorPickerPanel({ className, children, ...props }: ColorPickerPanelProps) {\n  return (\n    <div\n      data-control-ui=\"color-picker\"\n      data-popup-kind=\"color-picker\"\n      data-control-family=\"popup\"\n      data-slot=\"panel\"\n      data-surface=\"floating\"\n      data-popup-part=\"surface\"\n      data-popup-static=\"\"\n      className={cn(\"grid w-64 gap-3 p-3\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function ColorPickerArea({ className, style, ...props }: ColorPickerAreaProps) {\n  const { hsva, setHsva, disabled } = useColorPicker();\n  const { areaRef, onPointerDown, dragging } = useColorArea((offset, rect) => {\n    if (disabled) return;\n    const { s, v } = pointToSaturationValue(rect, offset.x, offset.y);\n    setHsva({ s, v });\n  });\n\n  function axisKey(axis: \"s\" | \"v\", event: ReactKeyboardEvent) {\n    if (!event.shiftKey) return; // native step (1) handles the un-shifted arrows\n    const cur = axis === \"s\" ? hsva.s : hsva.v;\n    let delta = 0;\n    if (event.key === \"ArrowUp\" || event.key === \"ArrowRight\") delta = 10;\n    else if (event.key === \"ArrowDown\" || event.key === \"ArrowLeft\") delta = -10;\n    if (delta === 0) return;\n    event.preventDefault();\n    const next = Math.min(100, Math.max(0, cur + delta));\n    setHsva(axis === \"s\" ? { s: next } : { v: next });\n  }\n\n  return (\n    // biome-ignore lint/a11y/useSemanticElements: a 2D color surface is a labelled slider group, not a fieldset form group.\n    <div\n      ref={areaRef}\n      data-control-ui=\"color-picker\"\n      data-control-family=\"color-picker\"\n      data-slot=\"area\"\n      data-disabled={disabled ? \"true\" : undefined}\n      data-dragging={dragging ? \"true\" : undefined}\n      role=\"group\"\n      aria-label=\"Saturation and brightness\"\n      onPointerDown={disabled ? undefined : onPointerDown}\n      className={cn(\n        \"group relative h-40 w-full touch-none select-none overflow-hidden\",\n        disabled ? \"cursor-not-allowed\" : \"cursor-crosshair\",\n        className,\n      )}\n      style={{ ...style, backgroundColor: `hsl(${hsva.h} 100% 50%)` }}\n      {...props}\n    >\n      <span\n        aria-hidden\n        data-control-ui=\"color-picker\"\n        data-control-family=\"color-picker\"\n        data-slot=\"area-saturation\"\n        className=\"absolute inset-0\"\n      />\n      <span\n        aria-hidden\n        data-control-ui=\"color-picker\"\n        data-control-family=\"color-picker\"\n        data-slot=\"area-brightness\"\n        className=\"absolute inset-0\"\n      />\n      <input\n        type=\"range\"\n        aria-label=\"Saturation\"\n        className=\"sr-only\"\n        min={0}\n        max={100}\n        step={1}\n        value={Math.round(hsva.s)}\n        disabled={disabled}\n        onChange={(event) => setHsva({ s: Number(event.target.value) })}\n        onKeyDown={(event) => axisKey(\"s\", event)}\n      />\n      <input\n        type=\"range\"\n        aria-label=\"Brightness\"\n        className=\"sr-only\"\n        min={0}\n        max={100}\n        step={1}\n        value={Math.round(hsva.v)}\n        disabled={disabled}\n        onChange={(event) => setHsva({ v: Number(event.target.value) })}\n        onKeyDown={(event) => axisKey(\"v\", event)}\n      />\n      <ColorPickerAreaThumb style={{ left: `${hsva.s}%`, top: `${100 - hsva.v}%` }} />\n    </div>\n  );\n}\n\nexport function ColorPickerAreaThumb({ className, ...props }: ColorPickerAreaThumbProps) {\n  return (\n    <div\n      data-control-ui=\"color-picker\"\n      data-control-family=\"color-picker\"\n      data-slot=\"area-thumb\"\n      aria-hidden\n      className={cn(\"pointer-events-none absolute size-4 -translate-x-1/2 -translate-y-1/2\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function ColorPickerHue({ className, \"aria-label\": ariaLabel, \"aria-labelledby\": ariaLabelledBy, ...props }: ColorPickerHueProps) {\n  const { hsva, setHsva, disabled } = useColorPicker();\n  return (\n    <SliderPrimitive.Root\n      data-control-ui=\"color-picker\"\n      data-control-family=\"color-picker\"\n      data-slot=\"hue\"\n      aria-label={ariaLabelledBy === undefined ? (ariaLabel ?? \"Hue\") : ariaLabel}\n      aria-labelledby={ariaLabelledBy}\n      value={hsva.h}\n      min={0}\n      max={360}\n      step={1}\n      disabled={disabled}\n      onValueChange={(next) => setHsva({ h: Array.isArray(next) ? next[0] : next })}\n      className={cn(\"relative flex h-4 w-full touch-none select-none items-center\", className)}\n      {...props}\n    >\n      <SliderPrimitive.Control className=\"flex h-4 w-full items-center\">\n        <SliderPrimitive.Track\n          data-control-ui=\"color-picker\"\n          data-control-family=\"color-picker\"\n          data-slot=\"hue-track\"\n          className=\"relative h-3 w-full\"\n          style={{ backgroundImage: HUE_GRADIENT }}\n        >\n          <SliderPrimitive.Thumb\n            data-control-ui=\"color-picker\"\n            data-control-family=\"color-picker\"\n            data-slot=\"hue-thumb\"\n            className={RING_THUMB}\n          />\n        </SliderPrimitive.Track>\n      </SliderPrimitive.Control>\n    </SliderPrimitive.Root>\n  );\n}\n\nexport function ColorPickerAlpha({\n  className,\n  \"aria-label\": ariaLabel,\n  \"aria-labelledby\": ariaLabelledBy,\n  ...props\n}: ColorPickerAlphaProps) {\n  const { hsva, setHsva, alpha, disabled } = useColorPicker();\n  if (!alpha) return null;\n  const opaque = formatColor({ ...hsva, a: 1 }, \"hex\");\n  return (\n    <SliderPrimitive.Root\n      data-control-ui=\"color-picker\"\n      data-control-family=\"color-picker\"\n      data-slot=\"alpha\"\n      aria-label={ariaLabelledBy === undefined ? (ariaLabel ?? \"Opacity\") : ariaLabel}\n      aria-labelledby={ariaLabelledBy}\n      value={hsva.a}\n      min={0}\n      max={1}\n      step={0.01}\n      disabled={disabled}\n      onValueChange={(next) => setHsva({ a: Array.isArray(next) ? next[0] : next })}\n      className={cn(\"relative flex h-4 w-full touch-none select-none items-center\", className)}\n      {...props}\n    >\n      <SliderPrimitive.Control className=\"flex h-4 w-full items-center\">\n        <SliderPrimitive.Track\n          data-control-ui=\"color-picker\"\n          data-control-family=\"color-picker\"\n          data-slot=\"alpha-track\"\n          className=\"relative h-3 w-full\"\n          style={{\n            backgroundImage: `linear-gradient(to right, transparent, ${opaque}), ${CHECKER}`,\n            backgroundSize: `auto, ${CHECKER_SIZE}`,\n          }}\n        >\n          <SliderPrimitive.Thumb\n            data-control-ui=\"color-picker\"\n            data-control-family=\"color-picker\"\n            data-slot=\"alpha-thumb\"\n            className={RING_THUMB}\n          />\n        </SliderPrimitive.Track>\n      </SliderPrimitive.Control>\n    </SliderPrimitive.Root>\n  );\n}\n\n// alternate to Area + Hue\n\nconst WHEEL_HUE =\n  \"conic-gradient(from 90deg, hsl(0 100% 50%), hsl(60 100% 50%), hsl(120 100% 50%), hsl(180 100% 50%), hsl(240 100% 50%), hsl(300 100% 50%), hsl(360 100% 50%))\";\n\nexport function ColorPickerWheel({ className, style, ...props }: ColorPickerWheelProps) {\n  const { hsva, setHsva, disabled } = useColorPicker();\n  const { areaRef, onPointerDown, dragging } = useColorArea((offset, rect) => {\n    if (disabled) return;\n    const { h, s } = pointToHueSat(rect, offset.x, offset.y);\n    setHsva({ h, s });\n  });\n  const radius = (hsva.s / 100) * 50;\n  const rad = (hsva.h * Math.PI) / 180;\n  const left = 50 + radius * Math.cos(rad);\n  const top = 50 + radius * Math.sin(rad);\n  return (\n    // biome-ignore lint/a11y/useSemanticElements: a 2D color surface is a labelled slider group, not a fieldset form group.\n    <div\n      ref={areaRef}\n      data-control-ui=\"color-picker\"\n      data-control-family=\"color-picker\"\n      data-slot=\"wheel\"\n      data-disabled={disabled ? \"true\" : undefined}\n      data-dragging={dragging ? \"true\" : undefined}\n      role=\"group\"\n      aria-label=\"Color wheel\"\n      onPointerDown={disabled ? undefined : onPointerDown}\n      className={cn(\n        \"group relative aspect-square w-full touch-none select-none\",\n        disabled ? \"cursor-not-allowed\" : \"cursor-crosshair\",\n        className,\n      )}\n      style={{ ...style, backgroundImage: `radial-gradient(circle at center, #fff, transparent 70%), ${WHEEL_HUE}` }}\n      {...props}\n    >\n      <div\n        data-control-ui=\"color-picker\"\n        data-control-family=\"color-picker\"\n        data-slot=\"wheel-thumb\"\n        aria-hidden\n        className=\"pointer-events-none absolute size-4 -translate-x-1/2 -translate-y-1/2\"\n        style={{ left: `${left}%`, top: `${top}%` }}\n      />\n      <input\n        type=\"range\"\n        aria-label=\"Wheel hue\"\n        className=\"sr-only\"\n        min={0}\n        max={360}\n        step={1}\n        value={Math.round(hsva.h)}\n        disabled={disabled}\n        onChange={(event) => setHsva({ h: Number(event.target.value) })}\n      />\n      <input\n        type=\"range\"\n        aria-label=\"Wheel saturation\"\n        className=\"sr-only\"\n        min={0}\n        max={100}\n        step={1}\n        value={Math.round(hsva.s)}\n        disabled={disabled}\n        onChange={(event) => setHsva({ s: Number(event.target.value) })}\n      />\n    </div>\n  );\n}\n\nexport function ColorPickerInput({\n  size = \"sm\",\n  className,\n  \"aria-label\": ariaLabel,\n  \"aria-labelledby\": ariaLabelledBy,\n  ...props\n}: ColorPickerInputProps) {\n  const { valueString, setFromString, disabled } = useColorPicker();\n  const [draft, setDraft] = useState(valueString);\n  const [editing, setEditing] = useState(false);\n  const shown = editing ? draft : valueString;\n  const commit = () => {\n    setFromString(draft);\n    setEditing(false);\n  };\n  return (\n    <Input\n      size={size}\n      value={shown}\n      disabled={disabled}\n      data-control-ui=\"color-picker\"\n      data-control-family=\"color-picker\"\n      data-slot=\"input\"\n      spellCheck={false}\n      autoComplete=\"off\"\n      aria-label={ariaLabelledBy === undefined ? (ariaLabel ?? \"Color value\") : ariaLabel}\n      aria-labelledby={ariaLabelledBy}\n      className={className}\n      onFocus={() => {\n        setDraft(valueString);\n        setEditing(true);\n      }}\n      onChange={(event) => setDraft(event.target.value)}\n      onBlur={commit}\n      onKeyDown={(event) => {\n        if (event.key === \"Enter\") commit();\n        else if (event.key === \"Escape\") setEditing(false);\n      }}\n      {...props}\n    />\n  );\n}\n\nexport function ColorPickerFormatSelect({\n  formats = [\"hex\", \"rgb\", \"hsl\", \"oklch\"],\n  size = \"sm\",\n  className,\n  \"aria-label\": ariaLabel,\n  \"aria-labelledby\": ariaLabelledBy,\n  style,\n}: ColorPickerFormatSelectProps) {\n  const { format, setFormat, disabled } = useColorPicker();\n  return (\n    <Select\n      value={format}\n      disabled={disabled}\n      onValueChange={(value) => {\n        if (isColorFormat(value)) setFormat(value);\n      }}\n    >\n      <SelectTrigger\n        size={size}\n        data-control-ui=\"color-picker\"\n        data-slot=\"format\"\n        aria-label={ariaLabelledBy === undefined ? (ariaLabel ?? \"Color format\") : ariaLabel}\n        aria-labelledby={ariaLabelledBy}\n        className={className}\n        style={style}\n      >\n        <SelectValue />\n      </SelectTrigger>\n      <SelectContent>\n        {formats.map((f) => (\n          <SelectItem key={f} value={f}>\n            {f.toUpperCase()}\n          </SelectItem>\n        ))}\n      </SelectContent>\n    </Select>\n  );\n}\n\nexport function ColorPickerChannels({ className, children, ...props }: ColorPickerChannelsProps) {\n  const { hsva, format, alpha } = useColorPicker();\n  const specs = getChannels(hsva, format).filter((spec) => alpha || spec.id !== \"a\");\n  return (\n    <div\n      data-control-ui=\"color-picker\"\n      data-control-family=\"color-picker\"\n      data-slot=\"channels\"\n      className={cn(\"grid grid-flow-col auto-cols-fr gap-1.5\", className)}\n      {...props}\n    >\n      {children ?? specs.map((spec) => <ColorPickerChannel key={spec.id} channel={spec.id} label={spec.label} />)}\n    </div>\n  );\n}\n\nexport function ColorPickerChannel({ channel, label, className, \"aria-label\": ariaLabel, ...props }: ColorPickerChannelProps) {\n  const { hsva, format, setChannelValue, disabled } = useColorPicker();\n  const spec = getChannels(hsva, format).find((s) => s.id === channel);\n  if (!spec) return null;\n  return (\n    <div\n      data-control-ui=\"color-picker\"\n      data-control-family=\"color-picker\"\n      data-slot=\"channel\"\n      className={cn(\"grid gap-1\", className)}\n      {...props}\n    >\n      <NumberField\n        size=\"sm\"\n        value={spec.value}\n        min={spec.min}\n        max={spec.max}\n        step={spec.step}\n        disabled={disabled}\n        onValueChange={(value) => {\n          if (value !== null) setChannelValue(channel, value);\n        }}\n      >\n        <NumberFieldGroup data-control-ui=\"color-picker\" data-control-family=\"color-picker\" data-slot=\"channel\" className=\"w-full\">\n          <NumberFieldInput aria-label={ariaLabel ?? `${CHANNEL_NAMES[channel]} channel`} className=\"px-1\" />\n        </NumberFieldGroup>\n      </NumberField>\n      <span data-control-ui=\"color-picker\" data-control-family=\"color-picker\" data-slot=\"channel-label\">\n        {label ?? spec.label}\n      </span>\n    </div>\n  );\n}\n\ndeclare global {\n  interface Window {\n    EyeDropper?: { new (): { open: () => Promise<{ sRGBHex: string }> } };\n  }\n}\n\nexport function ColorPickerEyeDropper({ className, children, ...props }: ColorPickerEyeDropperProps) {\n  const { setFromString, disabled } = useColorPicker();\n  const supported = useSyncExternalStore(subscribeEyeDropper, getEyeDropperSnapshot, getEyeDropperServerSnapshot);\n  if (!supported) return null;\n  const pick = async () => {\n    const Ctor = window.EyeDropper;\n    if (!Ctor) return;\n    try {\n      const result = await new Ctor().open();\n      setFromString(result.sRGBHex);\n    } catch {\n      // user cancelled — ignore\n    }\n  };\n  return (\n    <Button\n      variant=\"surface\"\n      size=\"sm\"\n      disabled={disabled}\n      aria-label=\"Pick a color from the screen\"\n      data-control-ui=\"color-picker\"\n      data-slot=\"eye-dropper\"\n      className={className}\n      onClick={pick}\n      {...props}\n    >\n      {children ?? <EyeDropperIcon />}\n    </Button>\n  );\n}\n\nexport function ColorPickerSwatches({ colors, label, className, children, ...props }: ColorPickerSwatchesProps) {\n  return (\n    <div data-control-ui=\"color-picker\" data-control-family=\"color-picker\" data-slot=\"swatches-group\" className=\"grid gap-1.5\">\n      {label ? (\n        <span data-control-ui=\"color-picker\" data-control-family=\"color-picker\" data-slot=\"swatches-label\">\n          {label}\n        </span>\n      ) : null}\n      <div\n        data-control-ui=\"color-picker\"\n        data-control-family=\"color-picker\"\n        data-slot=\"swatches\"\n        className={cn(\"flex flex-wrap gap-1.5\", className)}\n        {...props}\n      >\n        {colors?.map((color) => (\n          <ColorPickerSwatch key={color} color={color} />\n        ))}\n        {children}\n      </div>\n    </div>\n  );\n}\n\nexport function ColorPickerSwatch({ color, className, \"aria-label\": ariaLabel, ...props }: ColorPickerSwatchProps) {\n  const { setFromString, valueString } = useColorPicker();\n  const selected = sameColor(valueString, color);\n  return (\n    <button\n      type=\"button\"\n      data-control-ui=\"color-picker\"\n      data-control-family=\"color-picker\"\n      data-slot=\"swatch\"\n      data-selected={selected ? \"true\" : undefined}\n      aria-pressed={selected}\n      aria-label={ariaLabel ?? `Set color ${color}`}\n      title={color}\n      onClick={() => setFromString(color)}\n      className={cn(\"relative size-6 shrink-0 cursor-pointer overflow-hidden\", className)}\n      {...props}\n    >\n      <span\n        data-control-ui=\"color-picker\"\n        data-control-family=\"color-picker\"\n        data-slot=\"swatch-checker\"\n        aria-hidden\n        className=\"absolute inset-0\"\n        style={{ backgroundImage: CHECKER, backgroundSize: CHECKER_SIZE }}\n      />\n      <span\n        data-control-ui=\"color-picker\"\n        data-control-family=\"color-picker\"\n        data-slot=\"swatch-color\"\n        aria-hidden\n        className=\"absolute inset-0\"\n        style={{ backgroundColor: color }}\n      />\n    </button>\n  );\n}\n\nexport function ColorPickerSwatchAdd({ onAdd, className, children, ...props }: ColorPickerSwatchAddProps) {\n  const { valueString } = useColorPicker();\n  return (\n    <button\n      type=\"button\"\n      data-control-ui=\"color-picker\"\n      data-control-family=\"color-picker\"\n      data-slot=\"swatch-add\"\n      aria-label=\"Add current color\"\n      onClick={() => onAdd?.(valueString)}\n      className={cn(\"flex size-6 shrink-0 cursor-pointer items-center justify-center\", className)}\n      {...props}\n    >\n      {children ?? <PlusIcon />}\n    </button>\n  );\n}\n\nexport function ColorPickerContrast({ background = \"#ffffff\", className, ...props }: ColorPickerContrastProps) {\n  const { hsva, setFromString } = useColorPicker();\n  const bg = parseColor(background);\n  if (!bg) return null;\n  const bgRgba = hsvaToRgba(bg);\n  const ratio = contrastOf(hsva, bgRgba);\n  const levels = wcagLevels(ratio);\n  const target = nextFixLevel(levels);\n\n  const applyFix = () => {\n    if (!target) return;\n    const fixed = fixColorForContrast(hsva, bgRgba, TARGET_RATIO[target]);\n    if (fixed) setFromString(formatColor(fixed, \"hex\"));\n  };\n\n  return (\n    <div\n      data-control-ui=\"color-picker\"\n      data-control-family=\"color-picker\"\n      data-slot=\"contrast\"\n      className={cn(\"flex items-center gap-1.5\", className)}\n      {...props}\n    >\n      <span data-control-ui=\"color-picker\" data-control-family=\"color-picker\" data-slot=\"contrast-ratio\">\n        {ratio.toFixed(2)}:1\n      </span>\n      <WcagPill ok={levels.AA}>AA</WcagPill>\n      <WcagPill ok={levels.AAA}>AAA</WcagPill>\n      {target ? (\n        <button\n          type=\"button\"\n          data-control-ui=\"color-picker\"\n          data-control-family=\"color-picker\"\n          data-slot=\"contrast-fix\"\n          onClick={applyFix}\n          className=\"ml-auto cursor-pointer px-1.5 py-0.5\"\n        >\n          Fix → {target}\n        </button>\n      ) : null}\n    </div>\n  );\n}\n\nfunction WcagPill({ ok, children }: { ok: boolean; children: ReactNode }) {\n  return (\n    <span\n      data-control-ui=\"color-picker\"\n      data-control-family=\"color-picker\"\n      data-slot=\"contrast-level\"\n      data-passing={ok ? \"true\" : undefined}\n      className=\"inline-flex items-center gap-1 px-1.5 py-0.5\"\n    >\n      {ok ? \"✓\" : \"✕\"} {children}\n    </span>\n  );\n}\n\nexport function ColorPickerOutput({ className, children, renderValue, ...props }: ColorPickerOutputProps) {\n  const { valueString } = useColorPicker();\n  return (\n    <div\n      data-control-ui=\"color-picker\"\n      data-control-family=\"color-picker\"\n      data-slot=\"output\"\n      className={cn(\"flex items-center gap-2\", className)}\n      {...props}\n    >\n      {renderValue\n        ? renderValue({ value: valueString })\n        : (children ?? (\n            <>\n              <span\n                data-control-ui=\"color-picker\"\n                data-control-family=\"color-picker\"\n                data-slot=\"output-swatch\"\n                className=\"relative size-6 shrink-0 overflow-hidden\"\n              >\n                <span\n                  data-control-ui=\"color-picker\"\n                  data-control-family=\"color-picker\"\n                  data-slot=\"output-checker\"\n                  aria-hidden\n                  className=\"absolute inset-0\"\n                  style={{ backgroundImage: CHECKER, backgroundSize: CHECKER_SIZE }}\n                />\n                <span\n                  data-control-ui=\"color-picker\"\n                  data-control-family=\"color-picker\"\n                  data-slot=\"output-color\"\n                  aria-hidden\n                  className=\"absolute inset-0\"\n                  style={{ backgroundColor: valueString }}\n                />\n              </span>\n              <span data-control-ui=\"color-picker\" data-control-family=\"color-picker\" data-slot=\"output-value\">\n                {valueString}\n              </span>\n            </>\n          ))}\n    </div>\n  );\n}\n\nfunction sameColor(a: string, b: string): boolean {\n  const pa = parseColor(a);\n  const pb = parseColor(b);\n  if (!pa || !pb) return false;\n  return formatColor(pa, \"hex\", { alpha: true }) === formatColor(pb, \"hex\", { alpha: true });\n}\n\nfunction EyeDropperIcon() {\n  return (\n    <svg\n      viewBox=\"0 0 16 16\"\n      className=\"size-4\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"1.3\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      aria-hidden=\"true\"\n    >\n      <path d=\"M10.5 2.5a1.8 1.8 0 0 1 2.5 2.5l-1.3 1.3.9.9-1.1 1.1-.9-.9-4.4 4.4-2.2.6.6-2.2 4.4-4.4-.9-.9 1.1-1.1.9.9 1.3-1.3Z\" />\n    </svg>\n  );\n}\n\nfunction PlusIcon() {\n  return (\n    <svg\n      viewBox=\"0 0 16 16\"\n      className=\"size-3.5\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"1.5\"\n      strokeLinecap=\"round\"\n      aria-hidden=\"true\"\n    >\n      <path d=\"M8 3v10M3 8h10\" />\n    </svg>\n  );\n}\n"
    }
  ],
  "css": {
    "@import \"../components/control-ui/styles/recipes/color-picker.css\"": {},
    "@import \"../components/control-ui/styles/recipes/color-picker-details.css\"": {},
    "@import \"../components/control-ui/styles/recipes/color-picker-values.css\"": {},
    "@import \"../components/control-ui/styles/recipes/popup.css\"": {},
    "@import \"../components/control-ui/styles/recipes/popup-controls.css\"": {},
    "@import \"../components/control-ui/styles/recipes/popup-lists.css\"": {},
    "@import \"../components/control-ui/styles/recipes/popup-motion.css\"": {}
  },
  "meta": {}
}
