{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "color-picker-radix",
  "type": "registry:ui",
  "title": "Amplo Color Picker (Radix)",
  "description": "Radix/shadcn-classic shells for the Amplo color picker — Hue, Lightness, Alpha, FormatSwitcher, ChannelInput, Swatches, GamutBadge and ContrastReadout built on shadcn's Select and Tooltip. The OKLCH engine, contexts and pointer-driven parts come from the shared color engine, pulled in as a registry dependency.",
  "version": "2.0.0",
  "categories": [
    "color",
    "input",
    "design",
    "accessibility"
  ],
  "dependencies": [
    "culori",
    "@types/culori",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "select",
    "tooltip",
    "https://amplo.ale.design/r/color-picker-engine.json"
  ],
  "files": [
    {
      "path": "registry/new-york/ui/fill-picker/parts/hue.tsx",
      "type": "registry:ui",
      "target": "@ui/fill-picker/parts/hue.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { useColorPickerContext } from \"../context\";\nimport { hslHue, hsbHue } from \"../lib/color\";\nimport { setHueFromSlider } from \"../lib/channels\";\nimport { cn } from \"@/lib/utils\";\n\n// Slider-sourced hue never reaches 360: `setHueFromSlider` wraps 360 → 0, so\n// End / a drag past the right edge would snap the thumb back to the start\n// (End == Home). Clamping to just below the seam keeps the thumb at the end\n// and the rounded aria value at 360.\nconst HUE_SLIDER_MAX = 359.99;\n\nexport interface HueProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onKeyDown\"> {\n  orientation?: \"horizontal\" | \"vertical\";\n}\n\nexport const Hue = React.forwardRef<HTMLDivElement, HueProps>(function Hue(\n  { orientation = \"horizontal\", className, ...rest },\n  ref,\n) {\n  const { color, format, setColor } = useColorPickerContext();\n\n  // Slider tracks the active format's hue scale so the position matches the\n  // value shown in ChannelInput. OKLCH hue ≠ HSL/HSB hue for the same color\n  // (red is OKLCH ~29° / HSL 0°), so an OKLCH-driven slider feels broken to\n  // users editing in HSL/HSB. For formats without a hue channel (hex, rgb,\n  // p3, oklab) we fall back to OKLCH hue.\n  const displayedHue = React.useMemo(() => {\n    if (format === \"hsl\") return hslHue(color);\n    if (format === \"hsb\") return hsbHue(color);\n    return color.h;\n  }, [format, color]);\n  const trackRef = React.useRef<HTMLDivElement | null>(null);\n  React.useImperativeHandle(ref, () => trackRef.current as HTMLDivElement);\n\n  // Chroma rescaling and the HSL/HSB write path live in `setHueFromSlider`\n  // so this and the Base UI variant share one implementation.\n  const commitHue = React.useCallback(\n    (newH: number) =>\n      setColor(\n        setHueFromSlider(\n          color,\n          Math.max(0, Math.min(HUE_SLIDER_MAX, newH)),\n          format,\n        ),\n      ),\n    [color, format, setColor],\n  );\n\n  const moveTo = (clientCoord: number) => {\n    const el = trackRef.current;\n    if (!el) return;\n    const rect = el.getBoundingClientRect();\n    const ratio =\n      orientation === \"horizontal\"\n        ? (clientCoord - rect.left) / rect.width\n        : // Vertical is bottom-anchored (min at the bottom), matching the\n          // Base UI variant and ArrowUp = increase.\n          1 - (clientCoord - rect.top) / rect.height;\n    const clamped = Math.max(0, Math.min(1, ratio));\n    commitHue(clamped * 360);\n  };\n\n  const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {\n    (e.currentTarget as HTMLDivElement).setPointerCapture(e.pointerId);\n    moveTo(orientation === \"horizontal\" ? e.clientX : e.clientY);\n  };\n  const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {\n    if (e.buttons !== 1) return;\n    moveTo(orientation === \"horizontal\" ? e.clientX : e.clientY);\n  };\n  const releaseCapture = (e: React.PointerEvent<HTMLDivElement>) => {\n    const el = e.currentTarget as HTMLDivElement;\n    if (el.hasPointerCapture(e.pointerId)) el.releasePointerCapture(e.pointerId);\n  };\n\n  const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {\n    const big = e.shiftKey ? 10 : 1;\n    let next = displayedHue;\n    switch (e.key) {\n      case \"ArrowLeft\":\n      case \"ArrowDown\":\n        next -= big;\n        break;\n      case \"ArrowRight\":\n      case \"ArrowUp\":\n        next += big;\n        break;\n      case \"Home\":\n        next = 0;\n        break;\n      case \"End\":\n        next = 360;\n        break;\n      case \"PageUp\":\n        next += 25;\n        break;\n      case \"PageDown\":\n        next -= 25;\n        break;\n      default:\n        return;\n    }\n    e.preventDefault();\n    commitHue(next);\n  };\n\n  const pos = (((displayedHue % 360) + 360) % 360) / 360;\n  const isVertical = orientation === \"vertical\";\n\n  return (\n    <div\n      ref={trackRef}\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(displayedHue)}\n      aria-valuetext={`${Math.round(displayedHue)} degrees`}\n      aria-orientation={orientation}\n      tabIndex={0}\n      onPointerDown={onPointerDown}\n      onPointerMove={onPointerMove}\n      onPointerUp={releaseCapture}\n      onPointerCancel={releaseCapture}\n      onKeyDown={onKeyDown}\n      className={cn(\n        \"relative cursor-pointer rounded-full outline-none touch-none\",\n        // WCAG 2.5.8: the visual track stays 12px thin, but a pseudo-element\n        // widens the pointer target to 24px on the thin axis.\n        isVertical\n          ? \"h-32 w-3 before:absolute before:-inset-x-1.5 before:content-['']\"\n          : \"h-3 w-full before:absolute before:-inset-y-1.5 before:content-['']\",\n        \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-popover\",\n        className,\n      )}\n      style={{\n        background: isVertical\n          ? \"linear-gradient(to top, oklch(0.7 0.25 0), oklch(0.7 0.25 60), oklch(0.7 0.25 120), oklch(0.7 0.25 180), oklch(0.7 0.25 240), oklch(0.7 0.25 300), oklch(0.7 0.25 360))\"\n          : \"linear-gradient(to right, oklch(0.7 0.25 0), oklch(0.7 0.25 60), oklch(0.7 0.25 120), oklch(0.7 0.25 180), oklch(0.7 0.25 240), oklch(0.7 0.25 300), oklch(0.7 0.25 360))\",\n      }}\n      {...rest}\n    >\n      <div\n        className=\"pointer-events-none absolute size-4 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white shadow-[0_0_0_1.5px_rgba(0,0,0,0.6)]\"\n        style={\n          isVertical\n            ? { left: \"50%\", top: `calc((1 - ${pos}) * (100% - 16px) + 8px)`, background: `oklch(0.7 0.25 ${displayedHue})` }\n            : { left: `calc(${pos} * (100% - 16px) + 8px)`, top: \"50%\", background: `oklch(0.7 0.25 ${displayedHue})` }\n        }\n      />\n    </div>\n  );\n});\n"
    },
    {
      "path": "registry/new-york/ui/fill-picker/parts/lightness.tsx",
      "type": "registry:ui",
      "target": "@ui/fill-picker/parts/lightness.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { useColorPickerContext } from \"../context\";\nimport { formatColor } from \"../lib/color\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface LightnessProps\n  extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onKeyDown\"> {\n  orientation?: \"horizontal\" | \"vertical\";\n}\n\nexport const Lightness = React.forwardRef<HTMLDivElement, LightnessProps>(\n  function Lightness({ orientation = \"horizontal\", className, ...rest }, ref) {\n    const { color, setComponent } = useColorPickerContext();\n    const trackRef = React.useRef<HTMLDivElement | null>(null);\n    React.useImperativeHandle(ref, () => trackRef.current as HTMLDivElement);\n\n    const moveTo = (clientCoord: number) => {\n      const el = trackRef.current;\n      if (!el) return;\n      const rect = el.getBoundingClientRect();\n      const ratio =\n        orientation === \"horizontal\"\n          ? (clientCoord - rect.left) / rect.width\n          : // Vertical is bottom-anchored (min at the bottom), matching the\n            // Base UI variant and ArrowUp = increase.\n            1 - (clientCoord - rect.top) / rect.height;\n      const clamped = Math.max(0, Math.min(1, ratio));\n      setComponent(\"l\", clamped);\n    };\n\n    const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {\n      (e.currentTarget as HTMLDivElement).setPointerCapture(e.pointerId);\n      moveTo(orientation === \"horizontal\" ? e.clientX : e.clientY);\n    };\n    const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {\n      if (e.buttons !== 1) return;\n      moveTo(orientation === \"horizontal\" ? e.clientX : e.clientY);\n    };\n    const releaseCapture = (e: React.PointerEvent<HTMLDivElement>) => {\n      const el = e.currentTarget as HTMLDivElement;\n      if (el.hasPointerCapture(e.pointerId))\n        el.releasePointerCapture(e.pointerId);\n    };\n\n    const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {\n      const big = e.shiftKey ? 0.1 : 0.01;\n      let next = color.l;\n      switch (e.key) {\n        case \"ArrowLeft\":\n        case \"ArrowDown\":\n          next -= big;\n          break;\n        case \"ArrowRight\":\n        case \"ArrowUp\":\n          next += big;\n          break;\n        case \"Home\":\n          next = 0;\n          break;\n        case \"End\":\n          next = 1;\n          break;\n        case \"PageUp\":\n          next += 0.1;\n          break;\n        case \"PageDown\":\n          next -= 0.1;\n          break;\n        default:\n          return;\n      }\n      e.preventDefault();\n      setComponent(\"l\", next);\n    };\n\n    const isVertical = orientation === \"vertical\";\n    // Build the gradient at the current hue & chroma so users see how lightness\n    // changes _their_ color, not a generic black→white ramp.\n    const stops = React.useMemo(() => {\n      const samples = 8;\n      const arr: string[] = [];\n      for (let i = 0; i <= samples; i++) {\n        const l = i / samples;\n        arr.push(formatColor({ l, c: color.c, h: color.h, alpha: 1 }, \"oklch\"));\n      }\n      return arr.join(\", \");\n    }, [color.c, color.h]);\n\n    return (\n      <div\n        ref={trackRef}\n        data-slot=\"color-picker-lightness\"\n        role=\"slider\"\n        aria-label=\"Lightness\"\n        aria-valuemin={0}\n        aria-valuemax={100}\n        aria-valuenow={Math.round(color.l * 100)}\n        aria-valuetext={`${Math.round(color.l * 100)} percent`}\n        aria-orientation={orientation}\n        tabIndex={0}\n        onPointerDown={onPointerDown}\n        onPointerMove={onPointerMove}\n        onPointerUp={releaseCapture}\n        onPointerCancel={releaseCapture}\n        onKeyDown={onKeyDown}\n        className={cn(\n          \"relative cursor-pointer rounded-full outline-none touch-none\",\n          // WCAG 2.5.8: the visual track stays 12px thin, but a pseudo-element\n        // widens the pointer target to 24px on the thin axis.\n        isVertical\n          ? \"h-32 w-3 before:absolute before:-inset-x-1.5 before:content-['']\"\n          : \"h-3 w-full before:absolute before:-inset-y-1.5 before:content-['']\",\n          \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-popover\",\n          className,\n        )}\n        style={{\n          background: isVertical\n            ? `linear-gradient(to top, ${stops})`\n            : `linear-gradient(to right, ${stops})`,\n        }}\n        {...rest}\n      >\n        <div\n          className=\"pointer-events-none absolute size-4 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white shadow-[0_0_0_1.5px_rgba(0,0,0,0.6)]\"\n          style={\n            isVertical\n              ? {\n                  left: \"50%\",\n                  top: `calc((1 - ${color.l}) * (100% - 16px) + 8px)`,\n                  background: formatColor({ ...color, alpha: 1 }, \"oklch\"),\n                }\n              : {\n                  left: `calc(${color.l} * (100% - 16px) + 8px)`,\n                  top: \"50%\",\n                  background: formatColor({ ...color, alpha: 1 }, \"oklch\"),\n                }\n          }\n        />\n      </div>\n    );\n  },\n);\n"
    },
    {
      "path": "registry/new-york/ui/fill-picker/parts/alpha.tsx",
      "type": "registry:ui",
      "target": "@ui/fill-picker/parts/alpha.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { useColorPickerContext } from \"../context\";\nimport { formatColor } from \"../lib/color\";\nimport { cn } from \"@/lib/utils\";\nimport { CHECKERBOARD_LG } from \"../lib/constants\";\n\nexport interface AlphaProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onKeyDown\"> {\n  orientation?: \"horizontal\" | \"vertical\";\n}\n\nexport const Alpha = React.forwardRef<HTMLDivElement, AlphaProps>(function Alpha(\n  { orientation = \"horizontal\", className, ...rest },\n  ref,\n) {\n  const { color, setComponent } = useColorPickerContext();\n  const trackRef = React.useRef<HTMLDivElement | null>(null);\n  React.useImperativeHandle(ref, () => trackRef.current as HTMLDivElement);\n\n  const moveTo = (clientCoord: number) => {\n    const el = trackRef.current;\n    if (!el) return;\n    const rect = el.getBoundingClientRect();\n    const ratio =\n      orientation === \"horizontal\"\n        ? (clientCoord - rect.left) / rect.width\n        : // Vertical is bottom-anchored (min at the bottom), matching the\n          // Base UI variant and ArrowUp = increase.\n          1 - (clientCoord - rect.top) / rect.height;\n    setComponent(\"alpha\", Math.max(0, Math.min(1, ratio)));\n  };\n\n  const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {\n    (e.currentTarget as HTMLDivElement).setPointerCapture(e.pointerId);\n    moveTo(orientation === \"horizontal\" ? e.clientX : e.clientY);\n  };\n  const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {\n    if (e.buttons !== 1) return;\n    moveTo(orientation === \"horizontal\" ? e.clientX : e.clientY);\n  };\n  const releaseCapture = (e: React.PointerEvent<HTMLDivElement>) => {\n    const el = e.currentTarget as HTMLDivElement;\n    if (el.hasPointerCapture(e.pointerId)) el.releasePointerCapture(e.pointerId);\n  };\n  const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {\n    const big = e.shiftKey ? 0.1 : 0.01;\n    let next = color.alpha;\n    switch (e.key) {\n      case \"ArrowLeft\":\n      case \"ArrowDown\":\n        next -= big;\n        break;\n      case \"ArrowRight\":\n      case \"ArrowUp\":\n        next += big;\n        break;\n      case \"Home\":\n        next = 0;\n        break;\n      case \"End\":\n        next = 1;\n        break;\n      default:\n        return;\n    }\n    e.preventDefault();\n    setComponent(\"alpha\", next);\n  };\n\n  const isVertical = orientation === \"vertical\";\n  const opaque = formatColor({ ...color, alpha: 1 }, \"rgb\");\n  const transparent = formatColor({ ...color, alpha: 0 }, \"rgb\");\n\n  return (\n    <div\n      ref={trackRef}\n      data-slot=\"color-picker-alpha\"\n      role=\"slider\"\n      aria-label=\"Opacity\"\n      aria-valuemin={0}\n      aria-valuemax={100}\n      aria-valuenow={Math.round(color.alpha * 100)}\n      aria-valuetext={`${Math.round(color.alpha * 100)} percent`}\n      aria-orientation={orientation}\n      tabIndex={0}\n      onPointerDown={onPointerDown}\n      onPointerMove={onPointerMove}\n      onPointerUp={releaseCapture}\n      onPointerCancel={releaseCapture}\n      onKeyDown={onKeyDown}\n      className={cn(\n        \"relative cursor-pointer rounded-full outline-none touch-none\",\n        // WCAG 2.5.8: the visual track stays 12px thin, but a pseudo-element\n        // widens the pointer target to 24px on the thin axis.\n        isVertical\n          ? \"h-32 w-3 before:absolute before:-inset-x-1.5 before:content-['']\"\n          : \"h-3 w-full before:absolute before:-inset-y-1.5 before:content-['']\",\n        \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-popover\",\n        className,\n      )}\n      {...rest}\n    >\n      <div\n        aria-hidden=\"true\"\n        className=\"absolute inset-0 overflow-hidden rounded-full\"\n        style={{\n          backgroundImage: CHECKERBOARD_LG,\n          backgroundSize: \"12px 12px\",\n        }}\n      >\n        <div\n          className=\"absolute inset-0\"\n          style={{\n            background: isVertical\n              ? `linear-gradient(to top, ${transparent}, ${opaque})`\n              : `linear-gradient(to right, ${transparent}, ${opaque})`,\n          }}\n        />\n      </div>\n      <div\n        className=\"pointer-events-none absolute size-4 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white shadow-[0_0_0_1.5px_rgba(0,0,0,0.6)]\"\n        style={\n          isVertical\n            ? { left: \"50%\", top: `calc((1 - ${color.alpha}) * (100% - 16px) + 8px)`, background: opaque }\n            : { left: `calc(${color.alpha} * (100% - 16px) + 8px)`, top: \"50%\", background: opaque }\n        }\n      />\n    </div>\n  );\n});\n"
    },
    {
      "path": "registry/new-york/ui/fill-picker/parts/field-select.tsx",
      "type": "registry:ui",
      "target": "@ui/fill-picker/parts/field-select.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  Select,\n  SelectContent,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\";\n\nexport interface FieldSelectProps {\n  /**\n   * `standalone` (default) — bordered chevron-select matching every\n   * picker dropdown (`FormatSwitcher`, `TypeSwitcher`, `InterpSwitcher`,\n   * `RadialSizeSelect`).\n   *\n   * `inline` — borderless variant that lives inside a `FieldShell` (used\n   * by `ChannelInput`'s format toggle on the left).\n   */\n  variant?: \"standalone\" | \"inline\";\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  disabled?: boolean;\n  placeholder?: string;\n  /** Trigger button class — extends the variant's defaults. */\n  className?: string;\n  /** Props forwarded to the outer wrapper `<div>`. Use for `data-slot`,\n   *  width overrides, etc. */\n  wrapperProps?: React.HTMLAttributes<HTMLDivElement> & {\n    [key: `data-${string}`]: string | undefined;\n  };\n  /**\n   * Class applied to the popover `<SelectContent>`. Defaults to a\n   * `font-mono text-xs tracking-wide` block so item rows match the\n   * trigger font.\n   */\n  contentClassName?: string;\n  \"aria-label\"?: string;\n  /** Pass `<SelectItem>`s as children. */\n  children?: React.ReactNode;\n}\n\n/**\n * Single source of truth for every dropdown in the picker. Built on\n * shadcn `<Select>` (Base UI) so it composes cleanly with the rest of the\n * consumer's design system — focus rings, popover surface, item hover\n * states, and keyboard navigation are all the standard shadcn behaviors.\n *\n * Two variants:\n *   - `standalone` matches the bordered `h-8` field look of the other\n *     picker inputs (used by `FormatSwitcher`, `TypeSwitcher`).\n *   - `inline` lives inside a `FieldShell` (used by `ChannelInput`'s\n *     leading format toggle) — strips border/shadow/ring so the parent\n *     shell owns the chrome.\n *\n * The forwarded ref points at the `SelectTrigger` button so consumers\n * can imperatively focus it.\n */\nexport const FieldSelect = React.forwardRef<\n  HTMLButtonElement,\n  FieldSelectProps\n>(function FieldSelect(\n  {\n    variant = \"standalone\",\n    value,\n    defaultValue,\n    onValueChange,\n    disabled,\n    placeholder,\n    className,\n    wrapperProps,\n    contentClassName,\n    children,\n    \"aria-label\": ariaLabel,\n  },\n  ref,\n) {\n  const inline = variant === \"inline\";\n  const { className: wrapperClassName, ...wrapperRest } = wrapperProps ?? {};\n  return (\n    <div\n      className={cn(\n        inline\n          ? \"relative inline-flex h-full shrink-0 items-center\"\n          : \"relative inline-flex items-center\",\n        wrapperClassName,\n      )}\n      {...wrapperRest}\n    >\n      <Select\n        value={value}\n        defaultValue={defaultValue}\n        onValueChange={(v) => {\n          if (v != null) onValueChange?.(v as string);\n        }}\n        disabled={disabled}\n      >\n        <SelectTrigger\n          ref={ref}\n          size=\"sm\"\n          aria-label={ariaLabel}\n          className={cn(\n            \"font-mono text-xs tracking-wide\",\n            inline\n              ? \"h-full rounded-none border-0 bg-transparent px-2 shadow-none focus-visible:border-transparent focus-visible:ring-0 dark:bg-transparent dark:hover:bg-transparent\"\n              : \"w-full\",\n            className,\n          )}\n        >\n          <SelectValue placeholder={placeholder} />\n        </SelectTrigger>\n        <SelectContent\n          className={cn(\"font-mono text-xs tracking-wide\", contentClassName)}\n        >\n          {children}\n        </SelectContent>\n      </Select>\n    </div>\n  );\n});\n"
    },
    {
      "path": "registry/new-york/ui/fill-picker/parts/format-switcher.tsx",
      "type": "registry:ui",
      "target": "@ui/fill-picker/parts/format-switcher.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { SelectItem } from \"@/components/ui/select\";\nimport { cn } from \"@/lib/utils\";\nimport { useColorPickerContext } from \"../context\";\nimport type { ColorFormat } from \"../lib/types\";\nimport { FieldSelect } from \"./field-select\";\n\nexport interface FormatSwitcherProps {\n  /** Override the formats from <ColorPicker.Root formats={...} />. */\n  formats?: ColorFormat[];\n  className?: string;\n}\n\nexport const FormatSwitcher = React.forwardRef<\n  HTMLButtonElement,\n  FormatSwitcherProps\n>(function FormatSwitcher({ formats: formatsProp, className }, ref) {\n  const { format, setFormat, formats: ctxFormats } = useColorPickerContext();\n  const formats = formatsProp ?? ctxFormats;\n\n  return (\n    <FieldSelect\n      ref={ref}\n      aria-label=\"Color format\"\n      value={format}\n      onValueChange={(v) => setFormat(v as ColorFormat)}\n      className=\"w-full uppercase\"\n      wrapperProps={{\n        // w-full basis so the trigger's w-full has something to fill —\n        // without it the inline-flex wrapper sizes to the selected value\n        // and the control resizes per format (parity with the Base UI\n        // variant's FormatSwitcher).\n        className: cn(\"w-full\", className),\n        \"data-slot\": \"color-picker-format-switcher\",\n      }}\n    >\n      {formats.map((f) => (\n        <SelectItem key={f} value={f} className=\"uppercase\">\n          {f}\n        </SelectItem>\n      ))}\n    </FieldSelect>\n  );\n});\n"
    },
    {
      "path": "registry/new-york/ui/fill-picker/parts/channel-input.tsx",
      "type": "registry:ui",
      "target": "@ui/fill-picker/parts/channel-input.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { useColorPickerContext } from \"../context\";\nimport { parseColor } from \"../lib/color\";\nimport {\n  colorChannels,\n  setColorChannel,\n  type ChannelDescriptor,\n} from \"../lib/channels\";\nimport type { ColorFormat } from \"../lib/types\";\nimport { cn } from \"@/lib/utils\";\nimport { SelectItem } from \"@/components/ui/select\";\nimport {\n  FieldDivider,\n  FieldInput,\n  FieldInputGroup,\n  FieldShell,\n  FieldSuffix,\n} from \"./field\";\nimport { FieldSelect } from \"./field-select\";\n\nexport interface ChannelInputProps\n  extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onChange\"> {\n  /** Override the formats from <ColorPicker.Root formats={...}>. */\n  formats?: ColorFormat[];\n  /** Hide the inline format selector — useful when pairing with a\n   * standalone <FormatSwitcher /> elsewhere in the layout. */\n  showFormat?: boolean;\n}\n\n/**\n * Photoshop-style multi-field input: format selector on the left, one numeric\n * field per channel, alpha as %. For `hex` falls back to a single text field\n * (no meaningful per-channel breakdown). Pasting any CSS color string into any\n * field parses it and replaces the whole color.\n *\n * Built on the shared field primitives in `./field` so every text input in\n * the package shares the same border, focus ring, and chevron.\n */\nexport const ChannelInput = React.forwardRef<\n  HTMLDivElement,\n  ChannelInputProps\n>(function ChannelInput(\n  { formats: formatsProp, showFormat = true, className, ...rest },\n  ref,\n) {\n  const {\n    color,\n    format,\n    formatted,\n    setFormat,\n    setColor,\n    setFromString,\n    formats: ctxFormats,\n  } = useColorPickerContext();\n  const formats = formatsProp ?? ctxFormats;\n\n  const channels = React.useMemo(\n    () => colorChannels(color, format),\n    [color, format],\n  );\n\n  const handleChannelChange = (key: string, value: number) => {\n    setColor(setColorChannel(color, format, key, value));\n  };\n\n  return (\n    <FieldShell\n      ref={ref}\n      data-slot=\"color-picker-channel-input\"\n      className={className}\n      {...rest}\n    >\n      {showFormat && (\n        <>\n          <FieldSelect\n            aria-label=\"Color format\"\n            variant=\"inline\"\n            value={format}\n            onValueChange={(v) => setFormat(v as ColorFormat)}\n            className=\"uppercase\"\n            wrapperProps={{\n              \"data-slot\": \"color-picker-channel-input-format\",\n            }}\n          >\n            {formats.map((f) => (\n              <SelectItem key={f} value={f} className=\"uppercase\">\n                {f}\n              </SelectItem>\n            ))}\n          </FieldSelect>\n          <FieldDivider />\n        </>\n      )}\n      {format === \"hex\" ? (\n        <HexField value={formatted} onCommit={setFromString} />\n      ) : (\n        channels.map((ch, i) => (\n          <React.Fragment key={ch.key}>\n            <ChannelField\n              channel={ch}\n              onChange={(v) => handleChannelChange(ch.key, v)}\n              onPasteColor={setFromString}\n            />\n            {i < channels.length - 1 && <FieldDivider />}\n          </React.Fragment>\n        ))\n      )}\n    </FieldShell>\n  );\n});\n\n/* ────────────────────── Hex single field ────────────────────── */\n\nfunction HexField({\n  value,\n  onCommit,\n}: {\n  value: string;\n  onCommit: (v: string) => boolean;\n}) {\n  // Adjusting state during rendering — React-blessed alternative to\n  // syncing prop → state in useEffect. When the upstream `value` changes\n  // (e.g., a sibling control commits), we re-seed the draft synchronously\n  // *during* the render that observes the change, not after a paint.\n  const [draft, setDraft] = React.useState(value);\n  const [prevValue, setPrevValue] = React.useState(value);\n  const [error, setError] = React.useState(false);\n  if (value !== prevValue) {\n    setPrevValue(value);\n    setDraft(value);\n    setError(false);\n  }\n\n  const commit = (v: string) => {\n    const ok = onCommit(v.trim());\n    setError(!ok);\n  };\n\n  return (\n    <FieldInput\n      data-slot=\"color-picker-channel-input-field\"\n      aria-label=\"Hex value\"\n      aria-invalid={error || undefined}\n      value={draft}\n      onChange={(e) => {\n        setDraft(e.target.value);\n        setError(false);\n      }}\n      onBlur={(e) => commit(e.target.value)}\n      onKeyDown={(e) => {\n        if (e.key === \"Enter\") {\n          e.preventDefault();\n          commit(e.currentTarget.value);\n        } else if (e.key === \"Escape\") {\n          setDraft(value);\n          setError(false);\n        }\n      }}\n      className={cn(\n        \"flex-1 px-2 text-left\",\n        error && \"text-destructive\",\n      )}\n    />\n  );\n}\n\n/* ────────────────────── Numeric channel field ────────────────────── */\n\nfunction ChannelField({\n  channel,\n  onChange,\n  onPasteColor,\n}: {\n  channel: ChannelDescriptor;\n  onChange: (next: number) => void;\n  onPasteColor: (raw: string) => boolean;\n}) {\n  const display = formatNumber(channel.value, channel.precision);\n  // Sync draft when external state updates (slider drag, sibling channel,\n  // format swap) using the in-render adjustment pattern. Comparing formatted\n  // strings avoids clobbering an in-progress edit when the canonical value\n  // rounds to the same display string.\n  const [draft, setDraft] = React.useState(display);\n  const [prevDisplay, setPrevDisplay] = React.useState(display);\n  if (display !== prevDisplay) {\n    setPrevDisplay(display);\n    setDraft(display);\n  }\n\n  const commit = (raw: string) => {\n    const parsed = parseFloat(raw);\n    if (Number.isNaN(parsed)) {\n      setDraft(display);\n      return;\n    }\n    onChange(parsed);\n    // A clamped / rounded commit can leave `display` unchanged (\"999\" → 255\n    // when R is already 255), which the in-render sync never sees — snap the\n    // draft back here. If the commit did change `display`, the sync above\n    // overwrites this on the next render.\n    setDraft(display);\n  };\n\n  const step = (delta: number) => {\n    const parsed = parseFloat(draft);\n    const base = Number.isNaN(parsed) ? channel.value : parsed;\n    onChange(base + delta);\n  };\n\n  return (\n    <FieldInputGroup>\n      <span className=\"sr-only\">{channel.label}</span>\n      <FieldInput\n        data-slot=\"color-picker-channel-input-field\"\n        inputMode=\"decimal\"\n        aria-label={channel.label}\n        value={draft}\n        onChange={(e) => setDraft(e.target.value)}\n        onBlur={(e) => commit(e.target.value)}\n        onPaste={(e) => {\n          const text = e.clipboardData?.getData(\"text\") ?? \"\";\n          if (parseColor(text.trim())) {\n            e.preventDefault();\n            onPasteColor(text);\n          }\n        }}\n        onKeyDown={(e) => {\n          const big = e.shiftKey ? channel.bigStep : channel.step;\n          if (e.key === \"ArrowUp\") {\n            e.preventDefault();\n            step(big);\n          } else if (e.key === \"ArrowDown\") {\n            e.preventDefault();\n            step(-big);\n          } else if (e.key === \"Enter\") {\n            e.preventDefault();\n            commit(e.currentTarget.value);\n          } else if (e.key === \"Escape\") {\n            setDraft(display);\n          }\n        }}\n      />\n      {channel.suffix && <FieldSuffix>{channel.suffix}</FieldSuffix>}\n    </FieldInputGroup>\n  );\n}\n\n/* ────────────────────── Helpers ────────────────────── */\n\nfunction formatNumber(value: number, precision: number): string {\n  return precision === 0 ? String(Math.round(value)) : value.toFixed(precision);\n}\n"
    },
    {
      "path": "registry/new-york/ui/fill-picker/parts/swatches.tsx",
      "type": "registry:ui",
      "target": "@ui/fill-picker/parts/swatches.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Plus } from \"lucide-react\";\nimport { useColorPickerContext } from \"../context\";\nimport { formatColor, parseColor } from \"../lib/color\";\nimport type { OklchColor } from \"../lib/types\";\nimport { cn } from \"@/lib/utils\";\nimport { CHECKERBOARD_SM, SAMPLE_EDGE } from \"../lib/constants\";\nimport {\n  DEFAULT_SWATCH_PRESETS,\n  isSameSwatchColor,\n} from \"../lib/swatch-presets\";\n\nexport interface SwatchesProps extends React.HTMLAttributes<HTMLDivElement> {\n  presets?: string[];\n  /**\n   * When provided, renders a \"+\" tile after the presets that calls this with\n   * the current color. The consumer owns state (lift `presets` and update it\n   * here, persist to localStorage / a server / Zustand / whatever).\n   */\n  onAdd?: (color: OklchColor, hex: string) => void;\n}\n\nexport const Swatches = React.forwardRef<HTMLDivElement, SwatchesProps>(function Swatches(\n  { presets = DEFAULT_SWATCH_PRESETS, onAdd, className, ...rest },\n  ref,\n) {\n  const { color, setColor } = useColorPickerContext();\n  // Roving tabindex per the APG listbox pattern: Tab enters the group once,\n  // arrows move between options. Focus index survives re-renders but is\n  // clamped when the presets array shrinks.\n  const [focusIdx, setFocusIdx] = React.useState(0);\n  const optionRefs = React.useRef<Array<HTMLButtonElement | null>>([]);\n  const roveTo = (idx: number) => {\n    const clamped = Math.max(0, Math.min(idx, presets.length - 1));\n    setFocusIdx(clamped);\n    optionRefs.current[clamped]?.focus();\n  };\n  const onOptionKeyDown = (e: React.KeyboardEvent, i: number) => {\n    switch (e.key) {\n      case \"ArrowRight\":\n      case \"ArrowDown\":\n        e.preventDefault();\n        roveTo(i + 1);\n        break;\n      case \"ArrowLeft\":\n      case \"ArrowUp\":\n        e.preventDefault();\n        roveTo(i - 1);\n        break;\n      case \"Home\":\n        e.preventDefault();\n        roveTo(0);\n        break;\n      case \"End\":\n        e.preventDefault();\n        roveTo(presets.length - 1);\n        break;\n    }\n  };\n  const isSamePreset = (preset: OklchColor) =>\n    isSameSwatchColor(preset, color);\n  return (\n    <div\n      ref={ref}\n      data-slot=\"color-picker-swatches\"\n      role=\"listbox\"\n      aria-label=\"Color swatches\"\n      className={cn(\"grid grid-cols-10 gap-1\", className)}\n      {...rest}\n    >\n      {presets.map((p, i) => {\n        const parsed = parseColor(p);\n        const active = parsed ? isSamePreset(parsed) : false;\n        // Paint the swatch with the raw preset string so out-of-sRGB colors\n        // (P3 / OKLCH wide-gamut) actually render in their native gamut on\n        // capable displays. Hex-conversion would clamp them to sRGB.\n        return (\n          <button\n            key={`${p}-${i}`}\n            ref={(el) => {\n              optionRefs.current[i] = el;\n            }}\n            type=\"button\"\n            role=\"option\"\n            aria-selected={active}\n            aria-label={p}\n            tabIndex={i === Math.min(focusIdx, presets.length - 1) ? 0 : -1}\n            onKeyDown={(e) => onOptionKeyDown(e, i)}\n            onFocus={() => setFocusIdx(i)}\n            onClick={() => setColor(p)}\n            className={cn(\n              // before pseudo-padding: keep the 20px visual chip but give the\n              // button a 28px hit area (WCAG 2.5.8 target size).\n              \"relative size-5 cursor-pointer rounded-sm outline-none motion-safe:transition-transform\",\n              \"before:absolute before:-inset-1 before:content-['']\",\n              SAMPLE_EDGE,\n              \"focus-visible:ring-2 focus-visible:ring-ring motion-safe:hover:scale-110\",\n              active && \"ring-2 ring-ring\",\n            )}\n            style={{ backgroundImage: CHECKERBOARD_SM, backgroundSize: \"8px 8px\" }}\n          >\n            <span\n              aria-hidden\n              // No border to inset against any more, so the fill matches the\n              // tile's own radius exactly.\n              className=\"absolute inset-0 rounded-[inherit]\"\n              style={{ background: p }}\n            />\n          </button>\n        );\n      })}\n      {onAdd && (\n        <button\n          type=\"button\"\n          aria-label=\"Add current color to swatches\"\n          onClick={() => onAdd(color, formatColor(color, \"hex\"))}\n          className={cn(\n            \"relative inline-flex size-5 cursor-pointer items-center justify-center rounded-sm border border-dashed border-border text-muted-foreground outline-none motion-safe:transition-colors\",\n            \"before:absolute before:-inset-1 before:content-['']\",\n            \"hover:border-foreground hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\",\n          )}\n        >\n          <Plus className=\"size-3\" />\n        </button>\n      )}\n    </div>\n  );\n});\n"
    },
    {
      "path": "registry/new-york/ui/fill-picker/parts/gamut-badge.tsx",
      "type": "registry:ui",
      "target": "@ui/fill-picker/parts/gamut-badge.tsx",
      "content": "\"use client\";\n\n// Radix shell of GamutBadge: consumer's shadcn tooltip + asChild.\n// (The shadcn CLI rewrites asChild → render when installing into base-*\n// styles.) Shared logic lives in ./gamut-badge-shared.\n\nimport * as React from \"react\";\nimport { useColorPickerContext } from \"../context\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\nimport { GAMUT_BADGE_CLASS, gamutLabel } from \"./gamut-badge-shared\";\nimport type { GamutBadgeProps } from \"./gamut-badge-shared\";\n\nexport type { GamutBadgeProps };\n\nexport const GamutBadge = React.forwardRef<HTMLDivElement, GamutBadgeProps>(function GamutBadge(\n  { showLabel = true, className, ...rest },\n  ref,\n) {\n  const { gamut } = useColorPickerContext();\n  const label = gamutLabel(gamut);\n\n  return (\n    <TooltipProvider>\n      <Tooltip>\n        <TooltipTrigger asChild>\n          <div\n            ref={ref}\n            data-slot=\"color-picker-gamut-badge\"\n            role=\"status\"\n            aria-live=\"polite\"\n            tabIndex={0}\n            className={cn(GAMUT_BADGE_CLASS, className)}\n            {...rest}\n          >\n            {showLabel && <span className=\"text-muted-foreground\">Gamut</span>}\n            <span className=\"font-mono font-medium\">{label}</span>\n          </div>\n        </TooltipTrigger>\n        <TooltipContent>Color in {label} color space</TooltipContent>\n      </Tooltip>\n    </TooltipProvider>\n  );\n});\n"
    },
    {
      "path": "registry/new-york/ui/fill-picker/parts/contrast-readout.tsx",
      "type": "registry:ui",
      "target": "@ui/fill-picker/parts/contrast-readout.tsx",
      "content": "\"use client\";\n\n// Radix shell of ContrastReadout: consumer's shadcn tooltip + asChild.\n// (The shadcn CLI rewrites asChild → render when installing into base-*\n// styles.) All contrast logic lives in ./contrast-readout-shared.\n\nimport * as React from \"react\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  CONTRAST_READOUT_CLASS,\n  ContrastPopoverPanel,\n  useContrastReadout,\n} from \"./contrast-readout-shared\";\nimport type { ContrastMetric, ContrastReadoutProps } from \"./contrast-readout-shared\";\n\nexport type { ContrastMetric, ContrastReadoutProps };\n\nexport const ContrastReadout = React.forwardRef<HTMLDivElement, ContrastReadoutProps>(\n  function ContrastReadout(\n    { metrics, defaultMetric, showLabel, showValue, showBadges, className, ...rest },\n    ref,\n  ) {\n    const readout = useContrastReadout({\n      metrics,\n      defaultMetric,\n      showLabel,\n      showValue,\n      showBadges,\n    });\n\n    if (readout.togglable) {\n      return (\n        <TooltipProvider>\n          <Tooltip>\n            <TooltipTrigger asChild>\n              <button\n                ref={ref as React.Ref<HTMLButtonElement>}\n                data-slot=\"color-picker-contrast-readout\"\n                type=\"button\"\n                onClick={readout.cycle}\n                aria-label={`Contrast: ${readout.summary}. Click to switch to ${readout.nextMetric.toUpperCase()}.`}\n                className={cn(\n                  CONTRAST_READOUT_CLASS,\n                  \"cursor-pointer text-left motion-safe:transition-colors hover:bg-muted/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n                  className,\n                )}\n                {...(rest as React.ButtonHTMLAttributes<HTMLButtonElement>)}\n              >\n                {readout.body}\n                <span aria-hidden=\"true\" className=\"ml-auto text-muted-foreground\">⇅</span>\n                <span aria-live=\"polite\" className=\"sr-only\">\n                  {readout.cycleAnnouncement}\n                </span>\n              </button>\n            </TooltipTrigger>\n            <TooltipContent\n              side=\"top\"\n              align=\"center\"\n              className=\"max-w-[260px] bg-popover p-2.5 text-popover-foreground shadow-md\"\n            >\n              <ContrastPopoverPanel\n                title={readout.popover.title}\n                rows={readout.popover.rows}\n                fg={readout.fgCss}\n                bg={readout.bgCss}\n                footer={`Click to switch to ${readout.nextMetric.toUpperCase()}`}\n              />\n            </TooltipContent>\n          </Tooltip>\n        </TooltipProvider>\n      );\n    }\n\n    return (\n      <TooltipProvider>\n        <Tooltip>\n          <TooltipTrigger asChild>\n            <div\n              ref={ref}\n              data-slot=\"color-picker-contrast-readout\"\n              role=\"group\"\n              tabIndex={0}\n              aria-label={`Contrast against background: ${readout.summary}`}\n              className={cn(\n                CONTRAST_READOUT_CLASS,\n                \"cursor-default outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n                className,\n              )}\n              {...rest}\n            >\n              {readout.body}\n            </div>\n          </TooltipTrigger>\n          <TooltipContent\n            side=\"top\"\n            align=\"center\"\n            className=\"max-w-[260px] bg-popover p-2.5 text-popover-foreground shadow-md\"\n          >\n            <ContrastPopoverPanel\n              title={readout.popover.title}\n              rows={readout.popover.rows}\n              fg={readout.fgCss}\n              bg={readout.bgCss}\n            />\n          </TooltipContent>\n        </Tooltip>\n      </TooltipProvider>\n    );\n  },\n);\n"
    },
    {
      "path": "registry/new-york/ui/fill-picker/color-picker.tsx",
      "type": "registry:ui",
      "target": "@ui/fill-picker/color-picker.tsx",
      "content": "\"use client\";\n\nimport { Root } from \"./parts/root\";\nimport { Area } from \"./parts/area\";\nimport { Hue } from \"./parts/hue\";\nimport { Lightness } from \"./parts/lightness\";\nimport { Chroma } from \"./parts/chroma\";\nimport { Alpha } from \"./parts/alpha\";\nimport { CssInput } from \"./parts/css-input\";\nimport { FormatSwitcher } from \"./parts/format-switcher\";\nimport { ChannelInput } from \"./parts/channel-input\";\nimport { Swatches } from \"./parts/swatches\";\nimport { GamutBadge } from \"./parts/gamut-badge\";\nimport { ContrastReadout } from \"./parts/contrast-readout\";\nimport { Preview } from \"./parts/preview\";\nimport { EyeDropper } from \"./parts/eye-dropper\";\n\nexport type {\n  ColorFormat,\n  OklchColor,\n  GamutInfo,\n  ContrastResult,\n  Gamut,\n} from \"./lib/types\";\nexport type {\n  UseColorPickerProps,\n  ColorPickerState,\n} from \"./hooks/use-color-picker\";\nexport { useColorPicker } from \"./hooks/use-color-picker\";\nexport {\n  parseColor,\n  formatColor,\n  formatAll,\n  gamutInfo,\n  toGamut,\n  contrast,\n  apcaContrast,\n  isValidColor,\n} from \"./lib/color\";\nexport { colorChannels, setColorChannel } from \"./lib/channels\";\nexport type { ChannelDescriptor } from \"./lib/channels\";\n\n/**\n * Compositional color picker. Compose `<ColorPicker.Root>` with the named\n * parts to build the layout you want. Following shadcn convention there is\n * no kitchen-sink default component — see the docs for a copy-paste recipe\n * of the canonical layout.\n */\nexport const ColorPicker = {\n  Root,\n  Area,\n  Hue,\n  Lightness,\n  Chroma,\n  Alpha,\n  CssInput,\n  FormatSwitcher,\n  ChannelInput,\n  Swatches,\n  GamutBadge,\n  ContrastReadout,\n  Preview,\n  EyeDropper,\n};\n"
    }
  ]
}
