{ "$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}\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 * `` triggered by the swatch button). Extracted from Icon\n * Stack's background editor so apps share one accessible color-picking control\n * (Icon Stack solid/gradient stops, Chip Away custom accent). Presentational\n * and 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(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}, ref) {\n const inputRef = React.useRef(null);\n const swatch = size === \"sm\" ? \"w-7 h-7\" : \"w-8 h-8\";\n\n return (\n
\n {showHex && (\n \n {value}\n \n )}\n {\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-2 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 \n {icon}\n \n )}\n \n onChange(e.target.value)}\n disabled={disabled}\n className=\"sr-only\"\n aria-hidden=\"true\"\n tabIndex={-1}\n />\n
\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" } }