{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "color-field",
  "type": "registry:ui",
  "dependencies": [
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "color-field.tsx",
      "target": "components/ui/color-field.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst HEX = /^#[0-9a-fA-F]{6}$/;\n\nexport interface ColorFieldProps {\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (hex: string) => void;\n  label?: React.ReactNode;\n  swatchLabel?: string;\n  /** Accessible name for the hex input, used when `label` is absent. */\n  textLabel?: string;\n  disabled?: boolean;\n  className?: string;\n  id?: string;\n}\n\nexport const ColorField = React.forwardRef<HTMLInputElement, ColorFieldProps>(\n  (\n    {\n      value,\n      defaultValue = \"#000000\",\n      onValueChange,\n      label,\n      swatchLabel = \"Pick a color\",\n      textLabel = \"Hex colour value\",\n      disabled,\n      className,\n      id,\n    },\n    ref,\n  ) => {\n    const generatedId = React.useId();\n    const fieldId = id ?? generatedId;\n    const [draft, setDraft] = React.useState(value ?? defaultValue);\n    const [lastValue, setLastValue] = React.useState(value);\n\n    // The draft has to outrank a controlled value while typing: \"#5\" is not a\n    // valid hex, so onValueChange never fires for it, so value never comes back\n    // and the field would be uneditable.\n    if (value !== undefined && value !== lastValue) {\n      setLastValue(value);\n      setDraft(value);\n    }\n\n    const text = draft;\n    const invalid = text !== \"\" && !HEX.test(text);\n    const swatch = HEX.test(text) ? text : defaultValue;\n\n    const commit = (next: string) => {\n      setDraft(next);\n      if (HEX.test(next) || next === \"\") onValueChange?.(next);\n    };\n\n    return (\n      <div className={cn(\"flex items-center gap-2\", className)}>\n        {label ? (\n          <label htmlFor={fieldId} className=\"text-sm text-ink select-none\">\n            {label}\n          </label>\n        ) : null}\n        <input\n          type=\"color\"\n          aria-label={swatchLabel}\n          value={swatch}\n          disabled={disabled}\n          onChange={(e) => commit(e.target.value)}\n          className={cn(\n            \"size-6 shrink-0 cursor-pointer appearance-none rounded-sm border border-border bg-transparent p-0 outline-none\",\n            \"transition-control duration-base ease-soft\",\n            \"hover:border-border-strong\",\n            \"focus-visible:border-transparent focus-visible:ring-focus\",\n            \"disabled:cursor-not-allowed disabled:opacity-50\",\n            \"[&::-moz-color-swatch]:rounded-[3px] [&::-moz-color-swatch]:border-none\",\n            \"[&::-webkit-color-swatch]:rounded-[3px] [&::-webkit-color-swatch]:border-none\",\n            \"[&::-webkit-color-swatch-wrapper]:p-0\",\n          )}\n        />\n        <input\n          ref={ref}\n          id={fieldId}\n          type=\"text\"\n          spellCheck={false}\n          autoComplete=\"off\"\n          value={text}\n          disabled={disabled}\n          aria-label={label ? undefined : textLabel}\n          aria-invalid={invalid || undefined}\n          onChange={(e) => commit(e.target.value)}\n          className={cn(\n            \"w-24 rounded-lg border border-border bg-bg px-2 py-1 font-mono text-xs tabular-nums text-ink\",\n            \"outline-none transition-control duration-base ease-soft\",\n            \"hover:border-border-strong\",\n            \"focus-visible:border-transparent focus-visible:ring-focus\",\n            \"aria-invalid:border-danger aria-invalid:ring-2 aria-invalid:ring-danger/40\",\n            \"disabled:cursor-not-allowed disabled:opacity-50\",\n          )}\n        />\n      </div>\n    );\n  },\n);\nColorField.displayName = \"ColorField\";\n"
    }
  ]
}