{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "color-picker",
  "title": "Color Picker",
  "description": "Ridiculously typed color picker with oklch L×C pad, hue/alpha strips, and 6-mode round-trip (oklch, oklab, hex, rgb, hsl, hwb).",
  "dependencies": [],
  "registryDependencies": [
    "button",
    "popover"
  ],
  "files": [
    {
      "path": "src/components/ui/color-picker/index.ts",
      "content": "export type { ColorPickerProps } from \"./color-picker\"\nexport { ColorPicker } from \"./color-picker\"\nexport { isColorString } from \"./color-picker.helpers\"\nexport type {\n  ColorLiteral,\n  ColorMode,\n  ColorString,\n  ColorStringMap,\n  ColorValue,\n  HexLiteral,\n  HexString,\n  HSLLiteral,\n  HslString,\n  HWBLiteral,\n  HwbString,\n  ModeOf,\n  OKLCHLiteral,\n  OklabLiteral,\n  OklabString,\n  OklchString,\n  RGBALiteral,\n  RGBLiteral,\n  RgbString,\n  WithAlpha,\n  WithoutAlpha,\n} from \"./color-picker.types\"\nexport { color } from \"./color-picker.types\"\n",
      "type": "registry:ui",
      "target": "components/ui/color-picker/index.ts"
    },
    {
      "path": "src/components/ui/color-picker/color-picker.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useRef, useState } from \"react\"\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\"\nimport { cn } from \"@/lib/utils\"\nimport { AlphaStrip } from \"./alpha-strip\"\nimport { MAX_RECENTS, PRESETS } from \"./color-picker.constants\"\nimport {\n  formatColor,\n  formatHex,\n  parseColor,\n  parseHex,\n  pushRecent,\n  resolveCssColor,\n  srgbToOklch,\n} from \"./color-picker.helpers\"\nimport { useControllableState } from \"./color-picker.hooks\"\nimport type {\n  ColorMode,\n  ColorString,\n  ColorValue,\n  Oklch,\n} from \"./color-picker.types\"\nimport { HueStrip } from \"./hue-strip\"\nimport { LcPad } from \"./lc-pad\"\nimport { ModeButtonGroup } from \"./mode-button-group\"\nimport { type SwatchEntry, SwatchRow } from \"./swatch-row\"\n\nexport interface ColorPickerProps<\n  TMode extends ColorMode | undefined = undefined,\n> {\n  value: ColorString | (string & {})\n  onChange: (value: ColorValue<TMode>) => void\n  mode?: TMode\n  native?: boolean\n  className?: string\n  \"aria-label\"?: string\n  /**\n   * Preset swatches. Each entry is a color value OR a CSS variable\n   * (`\"var(--color-primary)\"` or bare `\"--color-primary\"`). `undefined` →\n   * the built-in palette; `[]` → no preset row. NOTE: this project's Tailwind\n   * v4 theme tokens are `--color-*`; pass the full name. A CSS-var entry must\n   * resolve to an sRGB-representable color (wide-gamut P3 tokens are skipped).\n   */\n  presets?: ReadonlyArray<ColorString | (string & {})>\n  /** Controlled recents. When provided, the component does not own this state. */\n  history?: ReadonlyArray<ColorValue<TMode>>\n  /** Uncontrolled initial recents. Ignored when `history` is provided. */\n  defaultHistory?: ReadonlyArray<ColorValue<TMode>>\n  /**\n   * Fired when recents change (a color committed on popover close). Fires in\n   * both modes. Passing `history` without `onHistoryChange` yields a frozen,\n   * read-only recents row.\n   */\n  onHistoryChange?: (history: ColorValue<TMode>[]) => void\n}\n\nexport function ColorPicker<TMode extends ColorMode | undefined>({\n  value,\n  onChange,\n  mode: modeProp,\n  native = false,\n  className,\n  \"aria-label\": ariaLabel = \"Pick a color\",\n  presets,\n  history,\n  defaultHistory,\n  onHistoryChange,\n}: ColorPickerProps<TMode>) {\n  const parsedFromValue = parseColor(value)\n\n  // Internal canonical oklch is the source of truth for the L×C pad / sliders.\n  // We do NOT derive marker position from each re-parse of `value`, because\n  // round-tripping through sRGB-bound modes (hex/rgb/hsl/hwb) gamut-clamps the\n  // oklch — the marker would refuse to follow the cursor into out-of-sRGB area.\n  // Instead: track internal oklch + the user's mode selection, and only resync\n  // from `value` when it changes from outside (not from our own emit).\n  const [internal, setInternal] = useState<Oklch>(\n    () => parsedFromValue?.oklch ?? { l: 0, c: 0, h: 0, a: 1 },\n  )\n  const [internalMode, setInternalMode] = useState<ColorMode>(\n    () => modeProp ?? parsedFromValue?.mode ?? \"oklch\",\n  )\n  const lastEmittedRef = useRef<string | null>(null)\n  const probeRef = useRef<HTMLSpanElement>(null)\n  const [hasEyeDropper, setHasEyeDropper] = useState(false)\n  const [open, setOpen] = useState(false)\n  const pendingRef = useRef<string | null>(null)\n  // History is stored internally as string[]; every ColorValue<TMode> member is\n  // a string, so the single boundary cast below is sound.\n  const [recents, setRecents] = useControllableState<readonly string[]>({\n    prop: history as readonly string[] | undefined,\n    defaultProp: (defaultHistory as readonly string[] | undefined) ?? [],\n    onChange: onHistoryChange as\n      | ((next: readonly string[]) => void)\n      | undefined,\n  })\n\n  useEffect(() => {\n    if (value === lastEmittedRef.current) return\n    const parsed = parseColor(value)\n    if (parsed) {\n      setInternal(parsed.oklch)\n      if (modeProp == null) setInternalMode(parsed.mode)\n    }\n  }, [value, modeProp])\n\n  useEffect(() => {\n    setHasEyeDropper(typeof window !== \"undefined\" && \"EyeDropper\" in window)\n  }, [])\n\n  if (native) {\n    const hex = parsedFromValue\n      ? formatHex(parsedFromValue.oklch, false)\n      : \"#000000\"\n    return (\n      <span\n        className={cn(\n          \"relative inline-block h-5 w-5 shrink-0 cursor-pointer rounded border outline-hidden focus-within:ring-2 focus-within:ring-ring\",\n          className,\n        )}\n        style={{ backgroundColor: value }}\n      >\n        <input\n          type=\"color\"\n          value={hex}\n          onChange={(event) => {\n            const next = parseHex(event.target.value)\n            if (!next) return\n            const oklch = srgbToOklch(next.r, next.g, next.b, next.a)\n            const formatted = formatColor(oklch, modeProp ?? \"hex\")\n            lastEmittedRef.current = formatted\n            onChange(formatted as Parameters<typeof onChange>[0])\n          }}\n          aria-label={ariaLabel}\n          data-slot=\"color-picker-native\"\n          className=\"absolute inset-0 h-full w-full cursor-pointer opacity-0\"\n        />\n      </span>\n    )\n  }\n\n  if (!parsedFromValue) {\n    return (\n      <span\n        aria-hidden=\"true\"\n        className={cn(\"inline-block h-5 w-5 rounded border\", className)}\n        style={{ backgroundColor: value }}\n        data-slot=\"color-picker-fallback\"\n      />\n    )\n  }\n\n  const activeMode: ColorMode = modeProp ?? internalMode\n  const showModeGroup = modeProp == null\n\n  const emit = (next: Oklch, mode: ColorMode = activeMode) => {\n    setInternal(next)\n    if (modeProp == null && mode !== internalMode) setInternalMode(mode)\n    const formatted = formatColor(next, mode)\n    lastEmittedRef.current = formatted\n    pendingRef.current = formatted\n    onChange(formatted as Parameters<typeof onChange>[0])\n  }\n\n  const commitRecent = (formatted: string) => {\n    setRecents((prev) => pushRecent(prev, formatted, MAX_RECENTS))\n  }\n\n  const handleOpenChange = (next: boolean) => {\n    setOpen(next)\n    if (next) {\n      pendingRef.current = null // start a fresh session\n      return\n    }\n    if (pendingRef.current) {\n      commitRecent(pendingRef.current)\n      pendingRef.current = null\n    }\n  }\n\n  const handlePickString = (raw: string) => {\n    const result = resolveCssColor(raw, probeRef.current)\n    if (result) emit(result.oklch)\n  }\n\n  const presetEntries: SwatchEntry[] =\n    presets === undefined\n      ? PRESETS.map((p) => ({\n          value: `oklch(${p.l} ${p.c} ${p.h})`,\n          label: p.name,\n        }))\n      : presets.map((p) => ({ value: p, label: p }))\n\n  const handleEyeDropper = async () => {\n    if (typeof window === \"undefined\") return\n    const EyeDropper = (\n      window as unknown as {\n        EyeDropper?: new () => { open(): Promise<{ sRGBHex: string }> }\n      }\n    ).EyeDropper\n    if (!EyeDropper) return\n    try {\n      const result = await new EyeDropper().open()\n      const parsed = parseHex(result.sRGBHex)\n      if (parsed) {\n        emit(srgbToOklch(parsed.r, parsed.g, parsed.b, 1))\n      }\n    } catch {\n      // user cancelled — ignore\n    }\n  }\n\n  return (\n    <Popover open={open} onOpenChange={handleOpenChange}>\n      <PopoverTrigger asChild>\n        <button\n          type=\"button\"\n          aria-label={ariaLabel}\n          className={cn(\n            \"shrink-0 cursor-pointer rounded outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n            className,\n          )}\n          data-slot=\"color-picker-trigger\"\n        >\n          <span\n            aria-hidden=\"true\"\n            className=\"block h-5 w-5 rounded border\"\n            style={{ backgroundColor: value }}\n          />\n        </button>\n      </PopoverTrigger>\n      <PopoverContent\n        className=\"w-fit min-w-65 p-3\"\n        align=\"start\"\n        data-slot=\"color-picker\"\n      >\n        <div className=\"flex flex-col gap-3\">\n          {showModeGroup && (\n            <ModeButtonGroup\n              mode={activeMode}\n              onChange={(next) => emit(internal, next)}\n            />\n          )}\n          {(hasEyeDropper || presetEntries.length > 0) && (\n            <div className=\"flex items-center gap-1.5\">\n              {hasEyeDropper && (\n                <button\n                  type=\"button\"\n                  onClick={handleEyeDropper}\n                  aria-label=\"Pick color from screen\"\n                  data-slot=\"color-picker-eyedropper\"\n                  className=\"flex h-5 w-5 shrink-0 cursor-pointer items-center justify-center rounded border bg-muted/40 text-muted-foreground transition hover:bg-muted hover:text-foreground\"\n                >\n                  <svg\n                    viewBox=\"0 0 16 16\"\n                    fill=\"none\"\n                    stroke=\"currentColor\"\n                    strokeWidth=\"1.5\"\n                    strokeLinecap=\"round\"\n                    strokeLinejoin=\"round\"\n                    className=\"h-3 w-3\"\n                    aria-hidden=\"true\"\n                  >\n                    <path d=\"m11.5 1.5 3 3-2 2-3-3z\" />\n                    <path d=\"m9.5 3.5-7 7v3h3l7-7\" />\n                  </svg>\n                </button>\n              )}\n              <SwatchRow\n                entries={presetEntries}\n                onPick={handlePickString}\n                ariaLabelPrefix=\"preset\"\n                dataSlot=\"color-picker-presets\"\n              />\n            </div>\n          )}\n          <SwatchRow\n            entries={recents.map((c) => ({ value: c, label: c }))}\n            onPick={handlePickString}\n            ariaLabelPrefix=\"recent\"\n            dataSlot=\"color-picker-recents\"\n          />\n          <LcPad\n            l={internal.l}\n            c={internal.c}\n            h={internal.h}\n            onChange={(l, c) => emit({ ...internal, l, c })}\n          />\n          <HueStrip h={internal.h} onChange={(h) => emit({ ...internal, h })} />\n          <AlphaStrip\n            a={internal.a}\n            l={internal.l}\n            c={internal.c}\n            h={internal.h}\n            onChange={(a) => emit({ ...internal, a })}\n          />\n          <span\n            ref={probeRef}\n            aria-hidden=\"true\"\n            style={{\n              position: \"absolute\",\n              height: 0,\n              width: 0,\n              overflow: \"hidden\",\n            }}\n          />\n        </div>\n      </PopoverContent>\n    </Popover>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/color-picker/color-picker.tsx"
    },
    {
      "path": "src/components/ui/color-picker/color-picker.helpers.ts",
      "content": "import type {\n  ColorLiteral,\n  ColorMode,\n  ColorString,\n  Oklch,\n} from \"./color-picker.types\"\n\n// ---------------------------------------------------------------------------\n// Parsing / formatting\n// ---------------------------------------------------------------------------\n\nexport function parseAlphaToken(raw: string): number {\n  return raw.endsWith(\"%\") ? parseFloat(raw) / 100 : parseFloat(raw)\n}\n\nexport function trimNumber(n: number): number {\n  return parseFloat(n.toFixed(4))\n}\n\nexport function clamp01(n: number): number {\n  return n < 0 ? 0 : n > 1 ? 1 : n\n}\n\nexport function parseHex(\n  value: string,\n): { r: number; g: number; b: number; a: number } | null {\n  const match = value.match(/^#([0-9a-f]{3,8})$/i)\n  if (!match) return null\n  const body = match[1]\n  const read = (hex: string) => parseInt(hex, 16) / 255\n  if (body.length === 3) {\n    return {\n      r: read(body[0].repeat(2)),\n      g: read(body[1].repeat(2)),\n      b: read(body[2].repeat(2)),\n      a: 1,\n    }\n  }\n  if (body.length === 4) {\n    return {\n      r: read(body[0].repeat(2)),\n      g: read(body[1].repeat(2)),\n      b: read(body[2].repeat(2)),\n      a: read(body[3].repeat(2)),\n    }\n  }\n  if (body.length === 6) {\n    return {\n      r: read(body.slice(0, 2)),\n      g: read(body.slice(2, 4)),\n      b: read(body.slice(4, 6)),\n      a: 1,\n    }\n  }\n  if (body.length === 8) {\n    return {\n      r: read(body.slice(0, 2)),\n      g: read(body.slice(2, 4)),\n      b: read(body.slice(4, 6)),\n      a: read(body.slice(6, 8)),\n    }\n  }\n  return null\n}\n\nexport function parseRgb(\n  value: string,\n): { r: number; g: number; b: number; a: number } | null {\n  const match = value.match(\n    /^rgba?\\(\\s*([\\d.]+%?)[\\s,]+([\\d.]+%?)[\\s,]+([\\d.]+%?)\\s*(?:[,/]\\s*([\\d.]+%?))?\\s*\\)$/i,\n  )\n  if (!match) return null\n  const channel = (raw: string) =>\n    raw.endsWith(\"%\") ? parseFloat(raw) / 100 : parseFloat(raw) / 255\n  const r = channel(match[1])\n  const g = channel(match[2])\n  const b = channel(match[3])\n  const a = match[4] != null ? parseAlphaToken(match[4]) : 1\n  if ([r, g, b].some((n) => Number.isNaN(n))) return null\n  return { r, g, b, a }\n}\n\nexport function parseHsl(\n  value: string,\n): { h: number; s: number; l: number; a: number } | null {\n  const match = value.match(\n    /^hsla?\\(\\s*([\\d.]+)(?:deg)?[\\s,]+([\\d.]+)%[\\s,]+([\\d.]+)%\\s*(?:[,/]\\s*([\\d.]+%?))?\\s*\\)$/i,\n  )\n  if (!match) return null\n  const h = parseFloat(match[1])\n  const s = parseFloat(match[2]) / 100\n  const l = parseFloat(match[3]) / 100\n  const a = match[4] != null ? parseAlphaToken(match[4]) : 1\n  if ([h, s, l].some((n) => Number.isNaN(n))) return null\n  return { h, s, l, a }\n}\n\nconst OKLCH_RE =\n  /^oklch\\(\\s*([\\d.]+%?)\\s+([\\d.]+%?)\\s+([\\d.]+(?:deg)?)\\s*(?:\\/\\s*([\\d.]+%?)\\s*)?\\)$/i\n\nexport function parseOklch(value: string): Oklch | null {\n  const match = value.match(OKLCH_RE)\n  if (!match) return null\n  const l = match[1].endsWith(\"%\")\n    ? parseFloat(match[1]) / 100\n    : parseFloat(match[1])\n  const c = match[2].endsWith(\"%\")\n    ? (parseFloat(match[2]) / 100) * 0.4\n    : parseFloat(match[2])\n  const h = parseFloat(match[3].replace(/deg$/i, \"\"))\n  const a = match[4] != null ? parseAlphaToken(match[4]) : 1\n  if ([l, c, h].some((n) => Number.isNaN(n))) return null\n  return { l, c, h, a }\n}\n\n// Alpha formatting: oklch and oklab preserve fractional precision via\n// trimNumber (perceptual spaces are precision-sensitive); rgb/hsl/hwb round\n// to integer percent (byte-quantized inputs make fractional alpha meaningless).\n// Hex encodes alpha as a byte-aligned digit pair.\n\nexport function formatOklch({ l, c, h, a }: Oklch): string {\n  const base = `oklch(${trimNumber(l)} ${trimNumber(c)} ${trimNumber(h)}`\n  if (a >= 1) return `${base})`\n  return `${base} / ${trimNumber(a * 100)}%)`\n}\n\nexport function formatHex(oklch: Oklch, includeAlpha: boolean): string {\n  const [r, g, b] = oklchToSrgb(oklch.l, oklch.c, oklch.h)\n  const channel = (n: number) =>\n    Math.round(clamp01(n) * 255)\n      .toString(16)\n      .padStart(2, \"0\")\n  const base = `#${channel(r)}${channel(g)}${channel(b)}`\n  if (!includeAlpha) return base\n  return `${base}${channel(oklch.a)}`\n}\n\nexport function formatRgb(oklch: Oklch): string {\n  const [r, g, b] = oklchToSrgb(oklch.l, oklch.c, oklch.h)\n  const channel = (n: number) => Math.round(clamp01(n) * 255)\n  const base = `rgb(${channel(r)} ${channel(g)} ${channel(b)}`\n  if (oklch.a >= 1) return `${base})`\n  return `${base} / ${Math.round(oklch.a * 100)}%)`\n}\n\nexport function formatHsl(oklch: Oklch): string {\n  const [r, g, b] = oklchToSrgb(oklch.l, oklch.c, oklch.h)\n  const hsl = srgbToHsl(clamp01(r), clamp01(g), clamp01(b))\n  const h = Math.round(hsl.h)\n  const s = Math.round(hsl.s * 100)\n  const l = Math.round(hsl.l * 100)\n  const base = `hsl(${h} ${s}% ${l}%`\n  if (oklch.a >= 1) return `${base})`\n  return `${base} / ${Math.round(oklch.a * 100)}%)`\n}\n\nconst OKLAB_RE =\n  /^oklab\\(\\s*([\\d.]+%?)\\s+(-?[\\d.]+%?)\\s+(-?[\\d.]+%?)\\s*(?:\\/\\s*([\\d.]+%?)\\s*)?\\)$/i\n\nexport function parseOklab(value: string): {\n  l: number\n  a: number\n  b: number\n  alpha: number\n} | null {\n  const match = value.match(OKLAB_RE)\n  if (!match) return null\n  const l = match[1].endsWith(\"%\")\n    ? parseFloat(match[1]) / 100\n    : parseFloat(match[1])\n  // Per CSS Color 4, oklab a/b percentages are relative to 0.4 (signed).\n  const a = match[2].endsWith(\"%\")\n    ? (parseFloat(match[2]) / 100) * 0.4\n    : parseFloat(match[2])\n  const b = match[3].endsWith(\"%\")\n    ? (parseFloat(match[3]) / 100) * 0.4\n    : parseFloat(match[3])\n  const alpha = match[4] != null ? parseAlphaToken(match[4]) : 1\n  if ([l, a, b].some((n) => Number.isNaN(n))) return null\n  return { l, a, b, alpha }\n}\n\nexport function formatOklab(oklch: Oklch): string {\n  const { a, b } = oklchToOklab(oklch.l, oklch.c, oklch.h)\n  const base = `oklab(${trimNumber(oklch.l)} ${trimNumber(a)} ${trimNumber(b)}`\n  if (oklch.a >= 1) return `${base})`\n  return `${base} / ${trimNumber(oklch.a * 100)}%)`\n}\n\nconst HWB_RE =\n  /^hwb\\(\\s*([\\d.]+)(?:deg)?[\\s,]+([\\d.]+)%[\\s,]+([\\d.]+)%\\s*(?:\\/\\s*([\\d.]+%?)\\s*)?\\)$/i\n\nexport function parseHwb(value: string): {\n  h: number\n  w: number\n  b: number\n  a: number\n} | null {\n  const match = value.match(HWB_RE)\n  if (!match) return null\n  const h = parseFloat(match[1])\n  const w = parseFloat(match[2]) / 100\n  const b = parseFloat(match[3]) / 100\n  const a = match[4] != null ? parseAlphaToken(match[4]) : 1\n  if ([h, w, b].some((n) => Number.isNaN(n))) return null\n  return { h, w, b, a }\n}\n\nexport function formatHwb(oklch: Oklch): string {\n  const [r, g, b] = oklchToSrgb(oklch.l, oklch.c, oklch.h)\n  const hwb = srgbToHwb(clamp01(r), clamp01(g), clamp01(b))\n  const h = Math.round(hwb.h)\n  const w = Math.round(hwb.w * 100)\n  const bk = Math.round(hwb.b * 100)\n  const base = `hwb(${h} ${w}% ${bk}%`\n  if (oklch.a >= 1) return `${base})`\n  return `${base} / ${Math.round(oklch.a * 100)}%)`\n}\n\n// ---------------------------------------------------------------------------\n// Color space conversions\n// ---------------------------------------------------------------------------\n\nexport function linearToSrgb(x: number): number {\n  if (x <= 0) return 0\n  if (x >= 1) return 1\n  return x <= 0.0031308 ? 12.92 * x : 1.055 * x ** (1 / 2.4) - 0.055\n}\n\nexport function srgbToLinear(x: number): number {\n  if (x <= 0) return 0\n  if (x >= 1) return 1\n  return x <= 0.04045 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4\n}\n\nexport function hslToSrgb(\n  h: number,\n  s: number,\n  l: number,\n): { r: number; g: number; b: number } {\n  const chroma = (1 - Math.abs(2 * l - 1)) * s\n  const hh = (((h % 360) + 360) % 360) / 60\n  const x = chroma * (1 - Math.abs((hh % 2) - 1))\n  let r1 = 0\n  let g1 = 0\n  let b1 = 0\n  if (hh < 1) [r1, g1, b1] = [chroma, x, 0]\n  else if (hh < 2) [r1, g1, b1] = [x, chroma, 0]\n  else if (hh < 3) [r1, g1, b1] = [0, chroma, x]\n  else if (hh < 4) [r1, g1, b1] = [0, x, chroma]\n  else if (hh < 5) [r1, g1, b1] = [x, 0, chroma]\n  else [r1, g1, b1] = [chroma, 0, x]\n  const m = l - chroma / 2\n  return { r: r1 + m, g: g1 + m, b: b1 + m }\n}\n\nexport function srgbToHsl(\n  r: number,\n  g: number,\n  b: number,\n): { h: number; s: number; l: number } {\n  const max = Math.max(r, g, b)\n  const min = Math.min(r, g, b)\n  const l = (max + min) / 2\n  const d = max - min\n  if (d === 0) return { h: 0, s: 0, l }\n  const s = d / (1 - Math.abs(2 * l - 1))\n  let h: number\n  if (max === r) h = ((g - b) / d) % 6\n  else if (max === g) h = (b - r) / d + 2\n  else h = (r - g) / d + 4\n  h *= 60\n  if (h < 0) h += 360\n  return { h, s, l }\n}\n\nexport function oklchToSrgb(\n  L: number,\n  C: number,\n  hDeg: number,\n): [number, number, number] {\n  const hRad = (hDeg * Math.PI) / 180\n  const a = C * Math.cos(hRad)\n  const b = C * Math.sin(hRad)\n\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\n  const lCubed = l_ * l_ * l_\n  const mCubed = m_ * m_ * m_\n  const sCubed = s_ * s_ * s_\n\n  const rLin =\n    4.0767416621 * lCubed - 3.3077115913 * mCubed + 0.2309699292 * sCubed\n  const gLin =\n    -1.2684380046 * lCubed + 2.6097574011 * mCubed - 0.3413193965 * sCubed\n  const bLin =\n    -0.0041960863 * lCubed - 0.7034186147 * mCubed + 1.707614701 * sCubed\n\n  return [linearToSrgb(rLin), linearToSrgb(gLin), linearToSrgb(bLin)]\n}\n\nexport function srgbToOklch(\n  r: number,\n  g: number,\n  b: number,\n  alpha: number,\n): Oklch {\n  const rLin = srgbToLinear(r)\n  const gLin = srgbToLinear(g)\n  const bLin = srgbToLinear(b)\n\n  const lLms = 0.4122214708 * rLin + 0.5363325363 * gLin + 0.0514459929 * bLin\n  const mLms = 0.2119034982 * rLin + 0.6806995451 * gLin + 0.1073969566 * bLin\n  const sLms = 0.0883024619 * rLin + 0.2817188376 * gLin + 0.6299787005 * bLin\n\n  const l_ = Math.cbrt(lLms)\n  const m_ = Math.cbrt(mLms)\n  const s_ = Math.cbrt(sLms)\n\n  const L = 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_\n  const aAxis = 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_\n  const bAxis = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_\n\n  const C = Math.sqrt(aAxis * aAxis + bAxis * bAxis)\n  let H = (Math.atan2(bAxis, aAxis) * 180) / Math.PI\n  if (H < 0) H += 360\n\n  return { l: L, c: C, h: H, a: alpha }\n}\n\nexport function oklchToOklab(\n  L: number,\n  C: number,\n  hDeg: number,\n): { l: number; a: number; b: number } {\n  const hRad = (hDeg * Math.PI) / 180\n  return { l: L, a: C * Math.cos(hRad), b: C * Math.sin(hRad) }\n}\n\nexport function oklabToOklch(\n  L: number,\n  a: number,\n  b: number,\n): { l: number; c: number; h: number } {\n  const C = Math.sqrt(a * a + b * b)\n  let H = (Math.atan2(b, a) * 180) / Math.PI\n  if (H < 0) H += 360\n  return { l: L, c: C, h: H }\n}\n\n// Per CSS Color 4 spec, https://www.w3.org/TR/css-color-4/#hwb-to-rgb\nexport function hwbToSrgb(\n  h: number,\n  w: number,\n  b: number,\n): { r: number; g: number; b: number } {\n  if (w + b >= 1) {\n    const gray = w / (w + b)\n    return { r: gray, g: gray, b: gray }\n  }\n  const rgb = hslToSrgb(h, 1, 0.5)\n  const scale = 1 - w - b\n  return {\n    r: rgb.r * scale + w,\n    g: rgb.g * scale + w,\n    b: rgb.b * scale + w,\n  }\n}\n\nexport function srgbToHwb(\n  r: number,\n  g: number,\n  b: number,\n): { h: number; w: number; b: number } {\n  const hsl = srgbToHsl(r, g, b)\n  const w = Math.min(r, g, b)\n  const bk = 1 - Math.max(r, g, b)\n  return { h: hsl.h, w, b: bk }\n}\n\n// ---------------------------------------------------------------------------\n// Mode dispatchers\n// ---------------------------------------------------------------------------\n\nexport interface ParseResult {\n  oklch: Oklch\n  mode: ColorMode\n}\n\nexport function parseColor(value: string): ParseResult | null {\n  const trimmed = value.trim()\n\n  const oklch = parseOklch(trimmed)\n  if (oklch) return { oklch, mode: \"oklch\" }\n\n  const oklab = parseOklab(trimmed)\n  if (oklab) {\n    const polar = oklabToOklch(oklab.l, oklab.a, oklab.b)\n    return {\n      oklch: { l: polar.l, c: polar.c, h: polar.h, a: oklab.alpha },\n      mode: \"oklab\",\n    }\n  }\n\n  const hex = parseHex(trimmed)\n  if (hex) {\n    return { oklch: srgbToOklch(hex.r, hex.g, hex.b, hex.a), mode: \"hex\" }\n  }\n\n  const rgb = parseRgb(trimmed)\n  if (rgb) {\n    return { oklch: srgbToOklch(rgb.r, rgb.g, rgb.b, rgb.a), mode: \"rgb\" }\n  }\n\n  const hsl = parseHsl(trimmed)\n  if (hsl) {\n    const srgb = hslToSrgb(hsl.h, hsl.s, hsl.l)\n    return {\n      oklch: srgbToOklch(srgb.r, srgb.g, srgb.b, hsl.a),\n      mode: \"hsl\",\n    }\n  }\n\n  const hwb = parseHwb(trimmed)\n  if (hwb) {\n    const srgb = hwbToSrgb(hwb.h, hwb.w, hwb.b)\n    return {\n      oklch: srgbToOklch(srgb.r, srgb.g, srgb.b, hwb.a),\n      mode: \"hwb\",\n    }\n  }\n\n  return null\n}\n\n/**\n * Runtime type guard for color strings. Narrows `string` to `ColorString`\n * and a literal `S` to `S & ColorLiteral<S>`.\n *\n * @example\n * const v: string = userInput\n * if (isColorString(v)) {\n *   // v is now typed ColorString\n * }\n */\nexport function isColorString(value: string): value is ColorString\nexport function isColorString<S extends string>(\n  value: S,\n): value is S & ColorLiteral<S>\nexport function isColorString(value: string): boolean {\n  return parseColor(value) !== null\n}\n\nexport function formatColor(oklch: Oklch, mode: ColorMode): string {\n  switch (mode) {\n    case \"oklch\":\n      return formatOklch(oklch)\n    case \"oklab\":\n      return formatOklab(oklch)\n    case \"hex\":\n      return formatHex(oklch, oklch.a < 1)\n    case \"rgb\":\n      return formatRgb(oklch)\n    case \"hsl\":\n      return formatHsl(oklch)\n    case \"hwb\":\n      return formatHwb(oklch)\n  }\n}\n\n// ---------------------------------------------------------------------------\n// CSS variable presets + recents\n// ---------------------------------------------------------------------------\n\n/** True for `var(--x)` / `var( --x )` and bare `--x` custom-property refs. */\nexport function isCssVar(s: string): boolean {\n  return /^\\s*(var\\(\\s*--|--)/.test(s)\n}\n\n/** A bare `--x` is wrapped to `var(--x)` for use as a CSS value; else passthrough. */\nexport function normalizeCssVar(s: string): string {\n  const trimmed = s.trim()\n  return trimmed.startsWith(\"--\") ? `var(${trimmed})` : s\n}\n\n/**\n * Resolve a preset/recent entry to a concrete color.\n * - Concrete colors parse directly (lossless; preserves wide-gamut oklch).\n * - CSS vars need the live cascade: set them on a mounted `probe` element and\n *   read the browser-serialized computed `color`, then parse that. Returns null\n *   when unresolvable (undefined var, no probe / server render). parseColor has\n *   no `color()` branch, so wide-gamut P3 tokens resolve to null by design.\n */\nexport function resolveCssColor(\n  raw: string,\n  probe: HTMLElement | null,\n): ParseResult | null {\n  if (!isCssVar(raw)) return parseColor(raw)\n  if (!probe) return null\n  probe.style.color = normalizeCssVar(raw)\n  const resolved = getComputedStyle(probe).color\n  probe.style.color = \"\"\n  return resolved ? parseColor(resolved) : null\n}\n\n/** Prepend `value`, drop any prior copy (move-to-front dedup), cap at `max`. */\nexport function pushRecent(\n  prev: readonly string[],\n  value: string,\n  max: number,\n): string[] {\n  return [value, ...prev.filter((c) => c !== value)].slice(0, max)\n}\n",
      "type": "registry:ui",
      "target": "components/ui/color-picker/color-picker.helpers.ts"
    },
    {
      "path": "src/components/ui/color-picker/color-picker.types.ts",
      "content": "// =====================================================================\n// 1. PRIMITIVES\n//\n// Char classes, trim, boolean logic, and number-shape / bounded-range\n// predicates are dogfooded from `ridiculous-type-kit` (this component used\n// to re-inline all of them verbatim). Only the COLOR-SPECIFIC composites\n// (alpha / rgb-channel / hue) are defined locally below.\n// =====================================================================\n\nimport type {\n  AllChars,\n  And,\n  HexDigit,\n  IsByte,\n  IsNonNegativeNumber,\n  IsNumber0To1,\n  IsNumber0To360,\n  IsNumber0To400,\n  IsPercent0To100,\n  IsSignedDecimal,\n  KeepIf,\n  Length,\n  Or,\n  Trim,\n} from \"@/lib/ridiculous-type-kit\"\n\ntype IsAlpha<S extends string> = Or<IsNumber0To1<S>, IsPercent0To100<S>>\n\ntype IsRgbChannel<S extends string> = Or<IsByte<S>, IsPercent0To100<S>>\n\ntype IsHue<S extends string> = S extends `${infer N}deg`\n  ? IsNumber0To360<N>\n  : S extends `${infer N}turn`\n    ? IsNumber0To1<N>\n    : S extends `${infer N}grad`\n      ? IsNumber0To400<N>\n      : IsNumber0To360<S>\n\n/**\n * The component's canonical runtime color value: OKLCH lightness/chroma/hue\n * plus alpha. This is the single source of truth the L×C pad and sliders read\n * and write; every mode formatter consumes it. Kept here (not in the helpers)\n * so both the component and the parse/format helpers can share one named shape.\n */\nexport interface Oklch {\n  l: number\n  c: number\n  h: number\n  a: number\n}\n\n// =====================================================================\n// 2. STRICT VALIDATORS — exported, generic. Used by color() helper.\n// =====================================================================\n\n/** #RGB, #RGBA, #RRGGBB, #RRGGBBAA */\nexport type HexLiteral<S extends string> = S extends `#${infer Body}`\n  ? Length<Body> extends 3 | 4 | 6 | 8\n    ? KeepIf<AllChars<Body, HexDigit>, S>\n    : never\n  : never\n\n/** rgb(255, 0, 0) | rgb(100%, 0%, 0%) | rgb(255 0 0) */\nexport type RGBLiteral<S extends string> =\n  S extends `rgb(${infer R},${infer G},${infer B})`\n    ? KeepIf<\n        And<\n          IsRgbChannel<Trim<R>>,\n          And<IsRgbChannel<Trim<G>>, IsRgbChannel<Trim<B>>>\n        >,\n        S\n      >\n    : S extends `rgb(${infer R} ${infer G} ${infer B})`\n      ? KeepIf<\n          And<\n            IsRgbChannel<Trim<R>>,\n            And<IsRgbChannel<Trim<G>>, IsRgbChannel<Trim<B>>>\n          >,\n          S\n        >\n      : never\n\n/**\n * rgba(255, 0, 0, 0.5) | rgba(255, 0, 0, 50%)\n * rgba(255 0 0 / 0.5) | rgb(255 0 0 / 0.5)\n */\nexport type RGBALiteral<S extends string> =\n  S extends `rgba(${infer R},${infer G},${infer B},${infer A})`\n    ? KeepIf<\n        And<\n          IsRgbChannel<Trim<R>>,\n          And<\n            IsRgbChannel<Trim<G>>,\n            And<IsRgbChannel<Trim<B>>, IsAlpha<Trim<A>>>\n          >\n        >,\n        S\n      >\n    : S extends `rgba(${infer R} ${infer G} ${infer B} / ${infer A})`\n      ? KeepIf<\n          And<\n            IsRgbChannel<Trim<R>>,\n            And<\n              IsRgbChannel<Trim<G>>,\n              And<IsRgbChannel<Trim<B>>, IsAlpha<Trim<A>>>\n            >\n          >,\n          S\n        >\n      : S extends `rgb(${infer R} ${infer G} ${infer B} / ${infer A})`\n        ? KeepIf<\n            And<\n              IsRgbChannel<Trim<R>>,\n              And<\n                IsRgbChannel<Trim<G>>,\n                And<IsRgbChannel<Trim<B>>, IsAlpha<Trim<A>>>\n              >\n            >,\n            S\n          >\n        : never\n\n/** hsl(210, 100%, 50%) | hsl(210 100% 50%) | hsl(210 100% 50% / 0.5) */\nexport type HSLLiteral<S extends string> =\n  S extends `hsl(${infer H} ${infer Sat} ${infer Light} / ${infer A})`\n    ? KeepIf<\n        And<\n          IsHue<Trim<H>>,\n          And<\n            IsPercent0To100<Trim<Sat>>,\n            And<IsPercent0To100<Trim<Light>>, IsAlpha<Trim<A>>>\n          >\n        >,\n        S\n      >\n    : S extends `hsl(${infer H},${infer Sat},${infer Light})`\n      ? KeepIf<\n          And<\n            IsHue<Trim<H>>,\n            And<IsPercent0To100<Trim<Sat>>, IsPercent0To100<Trim<Light>>>\n          >,\n          S\n        >\n      : S extends `hsl(${infer H} ${infer Sat} ${infer Light})`\n        ? KeepIf<\n            And<\n              IsHue<Trim<H>>,\n              And<IsPercent0To100<Trim<Sat>>, IsPercent0To100<Trim<Light>>>\n            >,\n            S\n          >\n        : never\n\n/**\n * oklch(62% 0.18 240)\n * oklch(0.62 0.18 240deg / 0.8)\n */\nexport type OKLCHLiteral<S extends string> =\n  S extends `oklch(${infer L} ${infer C} ${infer H} / ${infer A})`\n    ? KeepIf<\n        And<\n          Or<IsNumber0To1<Trim<L>>, IsPercent0To100<Trim<L>>>,\n          And<\n            Or<IsNonNegativeNumber<Trim<C>>, IsPercent0To100<Trim<C>>>,\n            And<IsHue<Trim<H>>, IsAlpha<Trim<A>>>\n          >\n        >,\n        S\n      >\n    : S extends `oklch(${infer L} ${infer C} ${infer H})`\n      ? KeepIf<\n          And<\n            Or<IsNumber0To1<Trim<L>>, IsPercent0To100<Trim<L>>>,\n            And<\n              Or<IsNonNegativeNumber<Trim<C>>, IsPercent0To100<Trim<C>>>,\n              IsHue<Trim<H>>\n            >\n          >,\n          S\n        >\n      : never\n\n/**\n * oklab(0.5 0.1 -0.05)\n * oklab(50% 0.1 -0.05 / 0.5)\n *\n * Note: a/b axes are signed (typically -0.4..0.4 in sRGB gamut). This validator\n * accepts any signed decimal; it does NOT range-check a/b to keep complexity\n * tractable. CSS Color 4 allows broader ranges anyway.\n */\nexport type OklabLiteral<S extends string> =\n  S extends `oklab(${infer L} ${infer A} ${infer B} / ${infer Alpha})`\n    ? KeepIf<\n        And<\n          Or<IsNumber0To1<Trim<L>>, IsPercent0To100<Trim<L>>>,\n          And<\n            IsSignedDecimal<Trim<A>>,\n            And<IsSignedDecimal<Trim<B>>, IsAlpha<Trim<Alpha>>>\n          >\n        >,\n        S\n      >\n    : S extends `oklab(${infer L} ${infer A} ${infer B})`\n      ? KeepIf<\n          And<\n            Or<IsNumber0To1<Trim<L>>, IsPercent0To100<Trim<L>>>,\n            And<IsSignedDecimal<Trim<A>>, IsSignedDecimal<Trim<B>>>\n          >,\n          S\n        >\n      : never\n\n/**\n * hwb(240 20% 30%)\n * hwb(240 20% 30% / 0.5)\n */\nexport type HWBLiteral<S extends string> =\n  S extends `hwb(${infer H} ${infer W} ${infer B} / ${infer A})`\n    ? KeepIf<\n        And<\n          IsHue<Trim<H>>,\n          And<\n            IsPercent0To100<Trim<W>>,\n            And<IsPercent0To100<Trim<B>>, IsAlpha<Trim<A>>>\n          >\n        >,\n        S\n      >\n    : S extends `hwb(${infer H} ${infer W} ${infer B})`\n      ? KeepIf<\n          And<\n            IsHue<Trim<H>>,\n            And<IsPercent0To100<Trim<W>>, IsPercent0To100<Trim<B>>>\n          >,\n          S\n        >\n      : never\n\nexport type ColorLiteral<S extends string> =\n  | HexLiteral<S>\n  | RGBLiteral<S>\n  | RGBALiteral<S>\n  | HSLLiteral<S>\n  | OKLCHLiteral<S>\n  | OklabLiteral<S>\n  | HWBLiteral<S>\n\n/** Validate a color literal at the call site. */\nexport const color = <S extends string>(value: S & ColorLiteral<S>): S => value\n\n// =====================================================================\n// 3. SUGGESTION STRINGS — non-generic, for IntelliSense + onChange returns.\n// =====================================================================\n\nexport type HexString = `#${string}`\n\nexport type RgbString =\n  | `rgb(${number} ${number} ${number})`\n  | `rgb(${number} ${number} ${number} / ${number}%)`\n  | `rgb(${number}, ${number}, ${number})`\n  | `rgba(${number}, ${number}, ${number}, ${number})`\n\nexport type HslString =\n  | `hsl(${number} ${number}% ${number}%)`\n  | `hsl(${number} ${number}% ${number}% / ${number}%)`\n\nexport type OklchString =\n  | `oklch(${number} ${number} ${number})`\n  | `oklch(${number} ${number} ${number} / ${number}%)`\n\nexport type OklabString =\n  | `oklab(${number} ${number} ${number})`\n  | `oklab(${number} ${number} ${number} / ${number}%)`\n\nexport type HwbString =\n  | `hwb(${number} ${number}% ${number}%)`\n  | `hwb(${number} ${number}% ${number}% / ${number}%)`\n\nexport type ColorString =\n  | HexString\n  | RgbString\n  | HslString\n  | OklchString\n  | OklabString\n  | HwbString\n\nexport interface ColorStringMap {\n  hex: HexString\n  rgb: RgbString\n  hsl: HslString\n  oklch: OklchString\n  oklab: OklabString\n  hwb: HwbString\n}\n\nexport type ColorMode = keyof ColorStringMap\n\n/**\n * The value `onChange` emits and the element type of the recents `history`.\n * Mode-locked pickers narrow to the exact mode string; auto pickers widen to\n * the full `ColorString` union. Reused so onChange and history can never drift.\n */\nexport type ColorValue<TMode extends ColorMode | undefined = undefined> =\n  TMode extends ColorMode ? ColorStringMap[TMode] : ColorString\n\n// =====================================================================\n// 4. UTILITY TYPES — operate on color string literals at the type level.\n// =====================================================================\n\n/**\n * Extract the color mode from a color literal at the type level.\n *\n * @example\n * type T1 = ModeOf<\"#ff0000\">              // \"hex\"\n * type T2 = ModeOf<\"oklch(0.5 0.1 240)\">   // \"oklch\"\n * type T3 = ModeOf<\"not-a-color\">          // never\n */\nexport type ModeOf<S extends string> = S extends `oklch(${string})`\n  ? \"oklch\"\n  : S extends `oklab(${string})`\n    ? \"oklab\"\n    : S extends `#${string}`\n      ? \"hex\"\n      : S extends `rgba(${string})`\n        ? \"rgb\"\n        : S extends `rgb(${string})`\n          ? \"rgb\"\n          : S extends `hsla(${string})`\n            ? \"hsl\"\n            : S extends `hsl(${string})`\n              ? \"hsl\"\n              : S extends `hwb(${string})`\n                ? \"hwb\"\n                : never\n\n/**\n * Replace (or add) the alpha tag on a color literal that uses functional\n * notation. Hex literals are NOT supported here because hex alpha is a\n * byte-aligned digit pair; encoding a number-to-hex transform at the type\n * level would require a 256-entry lookup table per nibble.\n *\n * @example\n * type T1 = WithAlpha<\"oklch(0.5 0.1 240)\", 50>           // \"oklch(0.5 0.1 240 / 50%)\"\n * type T2 = WithAlpha<\"oklch(0.5 0.1 240 / 100%)\", 50>    // \"oklch(0.5 0.1 240 / 50%)\"\n * type T3 = WithAlpha<\"rgb(255 0 0)\", 25>                 // \"rgb(255 0 0 / 25%)\"\n */\nexport type WithAlpha<\n  S extends string,\n  A extends number,\n> = S extends `${infer Base} / ${string})`\n  ? `${Base} / ${A}%)`\n  : S extends `${infer Base})`\n    ? `${Base} / ${A}%)`\n    : never\n\n/**\n * Strip the alpha tag from a color literal that uses functional notation.\n * Hex literals pass through unchanged (use `slice` at runtime for 8-digit\n * hex if needed).\n *\n * @example\n * type T1 = WithoutAlpha<\"oklch(0.5 0.1 240 / 50%)\">  // \"oklch(0.5 0.1 240)\"\n * type T2 = WithoutAlpha<\"oklch(0.5 0.1 240)\">        // \"oklch(0.5 0.1 240)\"\n */\nexport type WithoutAlpha<S extends string> =\n  S extends `${infer Base} / ${string})` ? `${Base})` : S\n",
      "type": "registry:ui",
      "target": "components/ui/color-picker/color-picker.types.ts"
    },
    {
      "path": "src/components/ui/color-picker/color-picker.hooks.ts",
      "content": "import { useCallback, useRef, useState } from \"react\"\n\ninterface UseControllableStateParams<T> {\n  /** Controlled value. When defined, the hook does not own the state. */\n  prop: T | undefined\n  /** Initial value used only in uncontrolled mode. */\n  defaultProp: T\n  /** Fired with the next value in BOTH controlled and uncontrolled modes. */\n  onChange?: (next: T) => void\n}\n\n/**\n * Minimal controlled/uncontrolled state. Self-contained (registry components\n * must not depend on @radix-ui/react-use-controllable-state). The setter is\n * stable and reads value/onChange through refs to avoid stale closures, and\n * accepts a functional updater.\n */\nexport function useControllableState<T>({\n  prop,\n  defaultProp,\n  onChange,\n}: UseControllableStateParams<T>): [T, (next: T | ((prev: T) => T)) => void] {\n  const [uncontrolled, setUncontrolled] = useState<T>(defaultProp)\n  const isControlled = prop !== undefined\n  const value = isControlled ? (prop as T) : uncontrolled\n\n  const valueRef = useRef(value)\n  valueRef.current = value\n  const isControlledRef = useRef(isControlled)\n  isControlledRef.current = isControlled\n  const onChangeRef = useRef(onChange)\n  onChangeRef.current = onChange\n\n  const setValue = useCallback((next: T | ((prev: T) => T)) => {\n    const resolved =\n      typeof next === \"function\"\n        ? (next as (prev: T) => T)(valueRef.current)\n        : next\n    if (!isControlledRef.current) setUncontrolled(resolved)\n    onChangeRef.current?.(resolved)\n  }, [])\n\n  return [value, setValue]\n}\n",
      "type": "registry:ui",
      "target": "components/ui/color-picker/color-picker.hooks.ts"
    },
    {
      "path": "src/components/ui/color-picker/alpha-strip.tsx",
      "content": "import { CHECKER_BG } from \"./color-picker.constants\"\nimport { clamp01 } from \"./color-picker.helpers\"\n\nexport function AlphaStrip({\n  a,\n  l,\n  c,\n  h,\n  onChange,\n}: {\n  a: number\n  l: number\n  c: number\n  h: number\n  onChange: (next: number) => void\n}) {\n  const handlePointer = (event: React.PointerEvent<HTMLDivElement>) => {\n    const rect = event.currentTarget.getBoundingClientRect()\n    onChange(clamp01((event.clientX - rect.left) / rect.width))\n  }\n\n  const color = `oklch(${l} ${c} ${h})`\n  const background = `linear-gradient(to right, transparent, ${color}), ${CHECKER_BG}`\n\n  return (\n    <div\n      data-slot=\"color-picker-alpha\"\n      role=\"slider\"\n      aria-label=\"Alpha\"\n      aria-valuemin={0}\n      aria-valuemax={100}\n      aria-valuenow={Math.round(a * 100)}\n      aria-valuetext={`${Math.round(a * 100)}%`}\n      tabIndex={0}\n      className=\"relative h-4 w-full cursor-pointer touch-none rounded-[3px]\"\n      style={{ background }}\n      onPointerDown={(event) => {\n        event.currentTarget.setPointerCapture(event.pointerId)\n        handlePointer(event)\n      }}\n      onPointerMove={(event) => {\n        if (event.buttons) handlePointer(event)\n      }}\n      onKeyDown={(event) => {\n        if (event.key === \"ArrowLeft\") onChange(clamp01(a - 0.01))\n        if (event.key === \"ArrowRight\") onChange(clamp01(a + 0.01))\n      }}\n    >\n      <div\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute top-1/2 h-5 w-1.5 -translate-x-1/2 -translate-y-1/2 rounded-full border border-black/40 bg-white shadow\"\n        style={{ left: `${a * 100}%` }}\n      />\n    </div>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/color-picker/alpha-strip.tsx"
    },
    {
      "path": "src/components/ui/color-picker/color-picker.constants.ts",
      "content": "import type { ColorMode } from \"./color-picker.types\"\n\n/** Modes offered by the in-popover format switcher, in display order. */\nexport const COLOR_MODES = [\n  \"oklch\",\n  \"oklab\",\n  \"hex\",\n  \"rgb\",\n  \"hsl\",\n  \"hwb\",\n] as const satisfies readonly ColorMode[]\n\n// L×C pad geometry. Width/height are the canvas pixel dimensions; chroma max\n// is the rightmost chroma value the horizontal axis maps to.\nexport const PAD_WIDTH = 240\nexport const PAD_HEIGHT = 160\nexport const CHROMA_MAX = 0.4\n\nexport const HUE_GRADIENT = `linear-gradient(to right, oklch(0.7 0.18 0), oklch(0.7 0.18 60), oklch(0.7 0.18 120), oklch(0.7 0.18 180), oklch(0.7 0.18 240), oklch(0.7 0.18 300), oklch(0.7 0.18 360))`\n\nexport const CHECKER_BG = `conic-gradient(#bbb 25%, #fff 0 50%, #bbb 0 75%, #fff 0) 0 0 / 10px 10px`\n\n/** Max recents kept in the history row (fits one popover-width swatch row). */\nexport const MAX_RECENTS = 8\n\nexport const PRESETS: ReadonlyArray<{\n  l: number\n  c: number\n  h: number\n  name: string\n}> = [\n  { l: 0.637, c: 0.237, h: 25.331, name: \"red\" },\n  { l: 0.705, c: 0.213, h: 47.604, name: \"orange\" },\n  { l: 0.769, c: 0.188, h: 70.08, name: \"amber\" },\n  { l: 0.768, c: 0.233, h: 130.85, name: \"lime\" },\n  { l: 0.696, c: 0.17, h: 162.48, name: \"emerald\" },\n  { l: 0.715, c: 0.143, h: 215.221, name: \"cyan\" },\n  { l: 0.623, c: 0.214, h: 259.815, name: \"blue\" },\n  { l: 0.606, c: 0.25, h: 292.717, name: \"violet\" },\n  { l: 0.667, c: 0.295, h: 322.15, name: \"fuchsia\" },\n  { l: 0.656, c: 0.241, h: 354.308, name: \"pink\" },\n]\n",
      "type": "registry:ui",
      "target": "components/ui/color-picker/color-picker.constants.ts"
    },
    {
      "path": "src/components/ui/color-picker/hue-strip.tsx",
      "content": "import { HUE_GRADIENT } from \"./color-picker.constants\"\nimport { clamp01 } from \"./color-picker.helpers\"\n\nexport function HueStrip({\n  h,\n  onChange,\n}: {\n  h: number\n  onChange: (h: number) => void\n}) {\n  const handlePointer = (event: React.PointerEvent<HTMLDivElement>) => {\n    const rect = event.currentTarget.getBoundingClientRect()\n    onChange(clamp01((event.clientX - rect.left) / rect.width) * 360)\n  }\n\n  return (\n    <div\n      data-slot=\"color-picker-hue\"\n      role=\"slider\"\n      aria-label=\"Hue\"\n      aria-valuemin={0}\n      aria-valuemax={360}\n      aria-valuenow={Math.round(h)}\n      aria-valuetext={`${Math.round(h)} degrees`}\n      tabIndex={0}\n      className=\"relative h-4 w-full cursor-pointer touch-none rounded-[3px]\"\n      style={{ background: HUE_GRADIENT }}\n      onPointerDown={(event) => {\n        event.currentTarget.setPointerCapture(event.pointerId)\n        handlePointer(event)\n      }}\n      onPointerMove={(event) => {\n        if (event.buttons) handlePointer(event)\n      }}\n      onKeyDown={(event) => {\n        if (event.key === \"ArrowLeft\") onChange(Math.max(0, h - 1))\n        if (event.key === \"ArrowRight\") onChange(Math.min(360, h + 1))\n      }}\n    >\n      <div\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute top-1/2 h-5 w-1.5 -translate-x-1/2 -translate-y-1/2 rounded-full border border-black/40 bg-white shadow\"\n        style={{ left: `${(h / 360) * 100}%` }}\n      />\n    </div>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/color-picker/hue-strip.tsx"
    },
    {
      "path": "src/components/ui/color-picker/lc-pad.tsx",
      "content": "import { useEffect, useRef } from \"react\"\nimport { CHROMA_MAX, PAD_HEIGHT, PAD_WIDTH } from \"./color-picker.constants\"\nimport { clamp01, oklchToSrgb } from \"./color-picker.helpers\"\n\nexport function LcPad({\n  l,\n  c,\n  h,\n  onChange,\n}: {\n  l: number\n  c: number\n  h: number\n  onChange: (l: number, c: number) => void\n}) {\n  const canvasRef = useRef<HTMLCanvasElement>(null)\n\n  useEffect(() => {\n    const canvas = canvasRef.current\n    if (!canvas) return\n    const ctx = canvas.getContext(\"2d\")\n    if (!ctx) return\n    const image = ctx.createImageData(PAD_WIDTH, PAD_HEIGHT)\n    for (let y = 0; y < PAD_HEIGHT; y++) {\n      const lAtRow = 1 - y / (PAD_HEIGHT - 1)\n      for (let x = 0; x < PAD_WIDTH; x++) {\n        const cAtCol = (x / (PAD_WIDTH - 1)) * CHROMA_MAX\n        const [r, g, b] = oklchToSrgb(lAtRow, cAtCol, h)\n        const i = (y * PAD_WIDTH + x) * 4\n        image.data[i] = r * 255\n        image.data[i + 1] = g * 255\n        image.data[i + 2] = b * 255\n        image.data[i + 3] = 255\n      }\n    }\n    ctx.putImageData(image, 0, 0)\n  }, [h])\n\n  const handlePointer = (event: React.PointerEvent<HTMLDivElement>) => {\n    const rect = event.currentTarget.getBoundingClientRect()\n    const nx = clamp01((event.clientX - rect.left) / rect.width)\n    const ny = clamp01((event.clientY - rect.top) / rect.height)\n    onChange(1 - ny, nx * CHROMA_MAX)\n  }\n\n  const markerX = (c / CHROMA_MAX) * 100\n  const markerY = (1 - l) * 100\n\n  // The pad is a 2-D control: lightness on the vertical axis, chroma on the\n  // horizontal. `role=\"application\"` carries no implicit value semantics and\n  // does not support `aria-valuetext`, so we fold the live value of BOTH axes\n  // into the accessible name itself. The label updates as the user nudges, so\n  // a screen reader announces the new lightness/chroma on every arrow press.\n  const label = `Lightness and chroma. Lightness ${(l * 100).toFixed(0)} percent, chroma ${c.toFixed(2)}. Use arrow keys to adjust lightness up and down, chroma left and right.`\n\n  return (\n    <div\n      data-slot=\"color-picker-pad\"\n      role=\"application\"\n      aria-label={label}\n      // biome-ignore lint/a11y/noNoninteractiveTabindex: 2D pad needs to be keyboard-focusable for arrow-key nudge\n      tabIndex={0}\n      className=\"relative w-full cursor-crosshair touch-none overflow-hidden rounded border focus:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n      style={{ height: PAD_HEIGHT }}\n      onPointerDown={(event) => {\n        event.currentTarget.setPointerCapture(event.pointerId)\n        handlePointer(event)\n      }}\n      onPointerMove={(event) => {\n        if (event.buttons) handlePointer(event)\n      }}\n      onKeyDown={(event) => {\n        const step = event.shiftKey ? 0.05 : 0.01\n        if (event.key === \"ArrowLeft\") {\n          event.preventDefault()\n          onChange(l, Math.max(0, c - step * CHROMA_MAX))\n        } else if (event.key === \"ArrowRight\") {\n          event.preventDefault()\n          onChange(l, Math.min(CHROMA_MAX, c + step * CHROMA_MAX))\n        } else if (event.key === \"ArrowUp\") {\n          event.preventDefault()\n          onChange(Math.min(1, l + step), c)\n        } else if (event.key === \"ArrowDown\") {\n          event.preventDefault()\n          onChange(Math.max(0, l - step), c)\n        }\n      }}\n    >\n      <canvas\n        ref={canvasRef}\n        width={PAD_WIDTH}\n        height={PAD_HEIGHT}\n        className=\"block h-full w-full\"\n      />\n      <div\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute h-3 w-3 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white shadow ring-1 ring-black/40\"\n        style={{ left: `${markerX}%`, top: `${markerY}%` }}\n      />\n    </div>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/color-picker/lc-pad.tsx"
    },
    {
      "path": "src/components/ui/color-picker/mode-button-group.tsx",
      "content": "import { Button } from \"@/components/ui/button\"\nimport { COLOR_MODES } from \"./color-picker.constants\"\nimport type { ColorMode } from \"./color-picker.types\"\n\nexport function ModeButtonGroup({\n  mode,\n  onChange,\n}: {\n  mode: ColorMode\n  onChange: (next: ColorMode) => void\n}) {\n  return (\n    <div\n      role=\"tablist\"\n      aria-label=\"Color format\"\n      className=\"flex w-full items-center justify-evenly gap-1\"\n      data-slot=\"color-picker-modes\"\n    >\n      {COLOR_MODES.map((m) => (\n        <Button\n          key={m}\n          type=\"button\"\n          role=\"tab\"\n          aria-selected={m === mode}\n          size=\"sm\"\n          variant={m === mode ? \"secondary\" : \"ghost\"}\n          onClick={() => onChange(m)}\n          className=\"h-7 px-2 font-mono text-xs\"\n        >\n          {m}\n        </Button>\n      ))}\n    </div>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/color-picker/mode-button-group.tsx"
    },
    {
      "path": "src/components/ui/color-picker/swatch-row.tsx",
      "content": "import { normalizeCssVar } from \"./color-picker.helpers\"\n\nexport interface SwatchEntry {\n  value: string\n  label: string\n}\n\nexport interface SwatchRowProps {\n  entries: ReadonlyArray<SwatchEntry>\n  onPick: (value: string) => void\n  ariaLabelPrefix: string\n  dataSlot: string\n}\n\nexport function SwatchRow({\n  entries,\n  onPick,\n  ariaLabelPrefix,\n  dataSlot,\n}: SwatchRowProps) {\n  if (entries.length === 0) return null\n  return (\n    <div\n      data-slot={dataSlot}\n      className=\"flex w-full flex-1 flex-wrap items-center justify-evenly gap-1.5\"\n    >\n      {entries.map((entry) => (\n        <button\n          key={entry.value}\n          type=\"button\"\n          aria-label={`${ariaLabelPrefix} ${entry.label}`}\n          onClick={() => onPick(entry.value)}\n          className=\"h-5 w-5 shrink-0 cursor-pointer rounded border transition hover:scale-110\"\n          style={{ backgroundColor: normalizeCssVar(entry.value) }}\n        />\n      ))}\n    </div>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/color-picker/swatch-row.tsx"
    }
  ],
  "type": "registry:ui"
}