{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "color-input",
  "type": "registry:ui",
  "title": "ColorInput",
  "description": "Focusable swatch button that opens the OS-native color picker and returns the chosen hex.",
  "categories": [
    "forms"
  ],
  "registryDependencies": [
    "https://whiskeyjack.net/r/utils.json"
  ],
  "files": [
    {
      "path": "components/ui/color-input.tsx",
      "type": "registry:ui",
      "target": "components/ui/color-input.tsx",
      "content": "import * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface ColorInputProps {\n  /** Current color as a hex string (`#RRGGBB`). */\n  value: string;\n  /** Fired with the new hex when the user picks a color. */\n  onChange: (hex: string) => void;\n  /** Accessible label (required) -- callers pass a translated string. */\n  \"aria-label\": string;\n  /** Show the hex value as monospace text beside the swatch. Default false. */\n  showHex?: boolean;\n  /** Disable interaction (e.g. an auto-computed color). Default false. */\n  disabled?: boolean;\n  /** Swatch size: `md` (32px, default) or `sm` (28px). */\n  size?: \"sm\" | \"md\";\n  /**\n   * Icon centered over the swatch -- e.g. a paint brush to mark this as a\n   * free \"pick any color\" control rather than a fixed swatch. The icon stays\n   * legible over the chosen color (its tint flips with the swatch's luminance).\n   */\n  icon?: React.ReactNode;\n  /**\n   * Render the swatch with a transparent (card) background instead of `value`,\n   * for a \"no custom color chosen yet\" state. The picker still opens at `value`,\n   * and once the user picks, drop `unset` so the swatch fills with their color.\n   */\n  unset?: boolean;\n  /** Mark as the active selection -- draws the same neutral ring as a chosen swatch. */\n  selected?: boolean;\n  className?: string;\n  /** Inline styles for the root element. */\n  style?: React.CSSProperties;\n}\n\n/** WCAG-ish readable icon tint over a solid hex background. */\nfunction readableOn(hex: string): string {\n  const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());\n  if (!m) return \"#ffffff\";\n  const n = parseInt(m[1], 16);\n  const lin = [(n >> 16) & 255, (n >> 8) & 255, n & 255].map((v) => {\n    const s = v / 255;\n    return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);\n  });\n  const L = 0.2126 * lin[0] + 0.7152 * lin[1] + 0.0722 * lin[2];\n  return L > 0.4 ? \"#1d1d1d\" : \"#ffffff\";\n}\n\n/**\n * A color swatch that opens the OS-native color picker (a hidden\n * `<input type=\"color\">` triggered by the swatch button). Shared by Icon Stack\n * (solid/gradient stops) and Chip Away (custom accent). Presentational and\n * i18n-agnostic -- pass a translated `aria-label`. The button is the single\n * focusable control (with a focus ring); the input is inert to AT.\n */\nexport const ColorInput = React.forwardRef<HTMLDivElement, ColorInputProps>(function ColorInput({\n  value,\n  onChange,\n  \"aria-label\": ariaLabel,\n  showHex = false,\n  disabled = false,\n  size = \"md\",\n  icon,\n  unset = false,\n  selected = false,\n  className,\n  style,\n}, ref) {\n  const inputRef = React.useRef<HTMLInputElement>(null);\n  const swatch = size === \"sm\" ? \"w-7 h-7\" : \"w-8 h-8\";\n\n  return (\n    <div ref={ref} className={cn(\"flex items-center gap-2\", className)} style={style}>\n      {showHex && (\n        <span className=\"text-xs font-mono text-[var(--color-text-muted-light)] dark:text-[var(--color-text-muted-dark)]\">\n          {value}\n        </span>\n      )}\n      <button\n        type=\"button\"\n        onClick={() => {\n          if (!disabled) inputRef.current?.click();\n        }}\n        aria-label={ariaLabel}\n        aria-disabled={disabled || undefined}\n        className={cn(\n          swatch,\n          \"wj-focus-ring flex items-center justify-center rounded-lg border transition-shadow [&_svg]:size-4\",\n          // Selected draws the same neutral high-contrast ring as a chosen swatch.\n          selected\n            ? \"border-[var(--color-text-primary-light)] ring-2 ring-offset-2 ring-[var(--color-text-primary-light)] ring-offset-[var(--color-surface-light)] dark:border-[var(--color-text-primary-dark)] dark:ring-[var(--color-text-primary-dark)] dark:ring-offset-[var(--color-surface-dark)]\"\n            : \"border-[var(--color-border-light)] dark:border-[var(--color-border-dark)]\",\n          // When unset the icon sits on the card background, so use a muted tint.\n          unset && \"text-[var(--color-text-muted-light)] dark:text-[var(--color-text-muted-dark)]\",\n          disabled ? \"cursor-not-allowed opacity-50\" : \"cursor-pointer hover:shadow-md\"\n        )}\n        style={{\n          backgroundColor: unset ? \"transparent\" : value,\n          // Over a filled swatch, flip the icon tint for contrast.\n          color: icon && !unset ? readableOn(value) : undefined,\n        }}\n      >\n        {icon && (\n          <span aria-hidden className=\"flex items-center justify-center\">\n            {icon}\n          </span>\n        )}\n      </button>\n      <input\n        ref={inputRef}\n        type=\"color\"\n        value={value}\n        onChange={(e) => onChange(e.target.value)}\n        disabled={disabled}\n        className=\"sr-only\"\n        aria-hidden=\"true\"\n        tabIndex={-1}\n      />\n    </div>\n  );\n});\n\nColorInput.displayName = \"ColorInput\"\n"
    }
  ],
  "docs": "Presentational and i18n-agnostic, so pass a translated aria-label. Use it for a custom accent or a gradient stop, and pair it with applyAccentColor to retint the whole UI from the chosen hex. `unset` renders the \"nothing chosen yet\" state while still opening the picker at `value`.",
  "meta": {
    "group": "forms",
    "related": [
      "accent-foreground"
    ],
    "exports": [
      "ColorInput"
    ],
    "siteSlug": "color-input"
  }
}
