{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "color-picker",
  "title": "Color Picker",
  "description": "A colour picker built from a hex/rgb/hsl text box and native hue, saturation and lightness sliders, with optional alpha and preset swatches. Reach for it wherever a person chooses a colour rather than a designer does: a theme or appearance editor, brand and accent colours in app settings, design-token and CSS-variable editors, label/tag and project colours, calendar-event and category colours, chart series colours, highlight and annotation colours, avatar and workspace backgrounds, status and priority colours, a whiteboard or drawing tool's palette, and admin panels that store a colour on a record. Common asks it answers: \"color picker\", \"colour picker\", \"hex color input\", \"color input field\", \"swatch picker\", \"hue slider\", \"rgb picker\", \"hsl picker\", \"alpha/opacity picker\", \"theme color editor\", \"react-colorful alternative\", \"react-color alternative\", \"shadcn color picker\" — the control usually pulled in as react-colorful, react-color, @uiw/react-color or an ad-hoc <input type=\"color\"> that gives you no keyboard story and no text entry. Official shadcn/ui ships no colour component of any kind: its slider is a Radix range primitive with no notion of colour, and input is a bare text box you would still have to parse and validate yourself. It settles the two things hand-rolled colour pickers get wrong. First, hue has to survive grey: with the hex string as the state of record, dragging lightness to 0 destroys the hue, so dragging back up returns red instead of the blue you started with — here h/s/l is the state and the hex is derived, so black still remembers it was blue, including under a controlled parent that echoes the value back. Second, a pasted hex has to come back out byte-identical: the conversion keeps full precision and rounds exactly once, so #123456 never drifts a digit just by being displayed (verified over all 16,777,216 sRGB colours). Beyond that: the box reads hex in all four widths (#abc, #abcd, #aabbcc, #aabbccdd, with or without the hash), rgb()/rgba() and hsl()/hsla() in both the comma and the modern space-with-slashed-alpha forms, and percentages wherever CSS allows them; out-of-range channels clamp and hues wrap the way a browser reads them, and blur rewrites the entry into canonical form so the reading is visible rather than silent. Text that cannot be read stays on screen to be fixed instead of being deleted, marks the field aria-invalid with a polite live message, and is withheld from onValueChange and from the hidden form input so nothing downstream has to validate twice. Colour names are refused by name rather than given a guessed value. The sliders are real range inputs, so arrow keys, Home/End and screen-reader value text come for free instead of being bolted onto a pointer-only saturation square; each is labelled and reports its own unit. Alpha is opt-in and round-trips through 8-digit hex; giving the field a name posts the colour through a hidden input; parseColor and formatColor are exported as plain functions for the rest of the app to share. One file, themed with shadcn tokens so it follows dark mode, no dependencies beyond React.",
  "files": [
    {
      "path": "registry/ui/color-picker.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/**\n * Why three sliders and not a saturation/value square: the square needs pointer\n * capture and element geometry, which makes it unusable from a keyboard without\n * a second, hidden set of controls. Native range inputs arrive with arrow keys,\n * Home/End, page steps and screen-reader support already attached, and they are\n * the axes people paste into a stylesheet anyway — hue, saturation, lightness is\n * literally CSS `hsl()`.\n *\n * Two things bite every hand-rolled color picker, and both are settled here:\n *\n *   - **Hue has to survive grey.** If the hex string is the state of record,\n *     dragging lightness to 0 destroys the hue — black is #000000 and nothing\n *     else — so dragging back up returns red instead of the blue you started\n *     with. Keeping h/s/l as the state and deriving the hex means black still\n *     remembers it was blue.\n *   - **A pasted hex has to come back out byte-identical.** Rounding the\n *     conversion on the way in makes #123456 drift a digit every time it is\n *     read, so a color quietly changes just by being displayed. The conversion\n *     keeps full precision internally and rounds exactly once, on the way out;\n *     the round trip is exact for all 16,777,216 sRGB colors.\n */\n\n/** What the field reads and writes. `hex` also covers the 8-digit form. */\nexport type ColorFormat = \"hex\" | \"rgb\" | \"hsl\"\n\n/** Hue 0–360, saturation and lightness 0–100, alpha 0–1. Kept unrounded. */\nexport interface Hsla {\n  h: number\n  s: number\n  l: number\n  a: number\n}\n\n/** Channels 0–255, alpha 0–1. */\nexport interface Rgba {\n  r: number\n  g: number\n  b: number\n  a: number\n}\n\n/** Why a string was rejected. Stable codes so the text can be translated. */\nexport type ColorErrorCode = \"invalid\" | \"named-color\"\n\nexport type ColorParseResult =\n  | { ok: true; color: Hsla }\n  | { ok: false; code: ColorErrorCode }\n\nconst clamp = (value: number, low: number, high: number) =>\n  value < low ? low : value > high ? high : value\n\n/**\n * Hue is the one axis that wraps rather than clamps — 400° is 40°, the way CSS\n * reads it — so a wrapped hue is not an error, just a different way to say the\n * same angle.\n */\nconst wrapHue = (value: number) => ((value % 360) + 360) % 360\n\nexport function rgbToHsl({ r, g, b, a }: Rgba): Hsla {\n  const rn = clamp(r, 0, 255) / 255\n  const gn = clamp(g, 0, 255) / 255\n  const bn = clamp(b, 0, 255) / 255\n  const max = Math.max(rn, gn, bn)\n  const min = Math.min(rn, gn, bn)\n  const chroma = max - min\n  const l = (max + min) / 2\n\n  // Grey has no hue and no saturation to speak of. Reporting 0 for both is the\n  // usual convention; the component keeps whatever hue the slider already held,\n  // which is the whole point of storing h/s/l rather than a hex string.\n  if (chroma === 0) return { h: 0, s: 0, l: l * 100, a }\n\n  let h: number\n  if (max === rn) h = ((gn - bn) / chroma) % 6\n  else if (max === gn) h = (bn - rn) / chroma + 2\n  else h = (rn - gn) / chroma + 4\n\n  return {\n    h: wrapHue(h * 60),\n    s: (chroma / (1 - Math.abs(2 * l - 1))) * 100,\n    l: l * 100,\n    a,\n  }\n}\n\nexport function hslToRgb({ h, s, l, a }: Hsla): Rgba {\n  const sn = clamp(s, 0, 100) / 100\n  const ln = clamp(l, 0, 100) / 100\n  const chroma = (1 - Math.abs(2 * ln - 1)) * sn\n  const sector = wrapHue(h) / 60\n  const second = chroma * (1 - Math.abs((sector % 2) - 1))\n  const base = ln - chroma / 2\n\n  let rgb: [number, number, number]\n  if (sector < 1) rgb = [chroma, second, 0]\n  else if (sector < 2) rgb = [second, chroma, 0]\n  else if (sector < 3) rgb = [0, chroma, second]\n  else if (sector < 4) rgb = [0, second, chroma]\n  else if (sector < 5) rgb = [second, 0, chroma]\n  else rgb = [chroma, 0, second]\n\n  // The single rounding step in the whole pipeline.\n  return {\n    r: Math.round((rgb[0] + base) * 255),\n    g: Math.round((rgb[1] + base) * 255),\n    b: Math.round((rgb[2] + base) * 255),\n    a,\n  }\n}\n\nconst NUMBER = /^[+-]?(?:\\d+\\.?\\d*|\\.\\d+)$/\n\n/** A plain number, or a percentage read against `full`. */\nfunction readValue(token: string, full: number): number | null {\n  const percent = token.endsWith(\"%\")\n  const body = percent ? token.slice(0, -1) : token\n  if (!NUMBER.test(body)) return null\n  const value = Number(body)\n  return percent ? (value / 100) * full : value\n}\n\nfunction readAlpha(token: string | undefined): number | null {\n  if (token === undefined) return 1\n  const value = readValue(token, 1)\n  return value === null ? null : clamp(value, 0, 1)\n}\n\nfunction parseHexBody(body: string): Rgba | null {\n  if (!/^[0-9a-f]+$/i.test(body)) return null\n  const short = body.length === 3 || body.length === 4\n  const long = body.length === 6 || body.length === 8\n  if (!short && !long) return null\n  const width = short ? 1 : 2\n  const channel = (index: number) => {\n    const chunk = body.slice(index * width, index * width + width)\n    return parseInt(short ? chunk + chunk : chunk, 16)\n  }\n  const withAlpha = body.length === 4 || body.length === 8\n  return {\n    r: channel(0),\n    g: channel(1),\n    b: channel(2),\n    a: withAlpha ? channel(3) / 255 : 1,\n  }\n}\n\n/**\n * Splits `rgb(…)` / `hsl(…)` arguments, accepting both the legacy comma form and\n * the modern space form with a slashed alpha.\n */\nfunction splitArguments(\n  body: string\n): { parts: string[]; alpha?: string } | null {\n  const [main, ...afterSlash] = body.split(\"/\")\n  if (afterSlash.length > 1) return null\n  const parts = main.trim().split(/[\\s,]+/).filter(Boolean)\n  // An empty alpha (\"rgb(1 2 3 /)\") needs no check of its own: it fails the\n  // number test downstream like any other unreadable alpha.\n  if (afterSlash.length === 1) return { parts, alpha: afterSlash[0].trim() }\n  if (parts.length === 4) return { parts: parts.slice(0, 3), alpha: parts[3] }\n  return { parts }\n}\n\nconst FUNCTIONAL = /^(rgba?|hsla?)\\((.*)\\)$/i\n\n/**\n * Reads a written color into unrounded HSL.\n *\n * Accepts hex in all four widths (`#abc`, `#abcd`, `#aabbcc`, `#aabbccdd`, with\n * or without the `#`), `rgb()`/`rgba()` and `hsl()`/`hsla()` in both the comma\n * and the space-with-slashed-alpha forms, and percentages wherever CSS allows\n * them. Out-of-range channels are clamped and hues wrap, the way a browser reads\n * them — the field then rewrites the entry on blur, so the reading is visible\n * rather than silent.\n *\n * CSS color *names* are refused by name rather than supported, because carrying\n * the 148-entry table into every project that installs this would cost more than\n * a picker whose output is always hex is worth.\n */\nexport function parseColor(input: string): ColorParseResult {\n  // Lowercased once, here: the hex digits, the function name and the bare-word\n  // check downstream all depend on it, and `HSL(…)` read as `rgb` is a silent\n  // wrong answer rather than a rejection.\n  const text = input.trim().toLowerCase()\n  const functional = FUNCTIONAL.exec(text)\n  if (!functional) {\n    // Hex is tried before the bare-word check, because \"abc\" and \"fff\" are hex\n    // that happens to be spelled with letters. No CSS colour name is made\n    // entirely of a–f at one of the four hex widths, so nothing is shadowed.\n    const rgb = parseHexBody(text.startsWith(\"#\") ? text.slice(1) : text)\n    if (rgb) return { ok: true, color: rgbToHsl(rgb) }\n    if (/^[a-z]+$/.test(text)) return { ok: false, code: \"named-color\" }\n    return { ok: false, code: \"invalid\" }\n  }\n\n  const kind = functional[1].startsWith(\"hsl\") ? \"hsl\" : \"rgb\"\n  const split = splitArguments(functional[2])\n  if (!split || split.parts.length !== 3) return { ok: false, code: \"invalid\" }\n  const alpha = readAlpha(split.alpha)\n  if (alpha === null) return { ok: false, code: \"invalid\" }\n\n  if (kind === \"rgb\") {\n    const channels = split.parts.map((part) => readValue(part, 255))\n    if (channels.some((value) => value === null))\n      return { ok: false, code: \"invalid\" }\n    const [r, g, b] = channels as number[]\n    return { ok: true, color: rgbToHsl({ r, g, b, a: alpha }) }\n  }\n\n  // `deg` is the only angle unit accepted; turns and radians are rare enough in\n  // pasted color that supporting them buys less than the extra surface costs.\n  const hueToken = split.parts[0].endsWith(\"deg\")\n    ? split.parts[0].slice(0, -3)\n    : split.parts[0]\n  const h = readValue(hueToken, 360)\n  const s = readValue(split.parts[1], 100)\n  const l = readValue(split.parts[2], 100)\n  if (h === null || s === null || l === null)\n    return { ok: false, code: \"invalid\" }\n  return {\n    ok: true,\n    color: { h: wrapHue(h), s: clamp(s, 0, 100), l: clamp(l, 0, 100), a: alpha },\n  }\n}\n\nconst hexPair = (value: number) => value.toString(16).padStart(2, \"0\")\n\n/** Two decimals is enough to name every alpha a slider or an 8-digit hex can hold. */\nconst roundAlpha = (a: number) => Math.round(clamp(a, 0, 1) * 100) / 100\n\n/**\n * Writes a color back out. Alpha is included only when the color actually has\n * some — an opaque color reads as `#3b82f6`, not `#3b82f6ff`.\n */\nexport function formatColor(color: Hsla, format: ColorFormat = \"hex\"): string {\n  const alpha = roundAlpha(color.a)\n\n  if (format === \"hsl\") {\n    const h = Math.round(wrapHue(color.h))\n    const s = Math.round(clamp(color.s, 0, 100))\n    const l = Math.round(clamp(color.l, 0, 100))\n    return alpha < 1\n      ? `hsl(${h} ${s}% ${l}% / ${alpha})`\n      : `hsl(${h} ${s}% ${l}%)`\n  }\n\n  const { r, g, b } = hslToRgb(color)\n  if (format === \"rgb\")\n    return alpha < 1 ? `rgb(${r} ${g} ${b} / ${alpha})` : `rgb(${r} ${g} ${b})`\n\n  // Hex decides on the byte it is about to write, not on the two-decimal alpha:\n  // rounding first would call 254/255 opaque and drop the channel that was asked\n  // for, so an 8-digit hex would stop surviving a round trip near the top of the\n  // range.\n  const byte = Math.round(clamp(color.a, 0, 1) * 255)\n  return byte < 255\n    ? `#${hexPair(r)}${hexPair(g)}${hexPair(b)}${hexPair(byte)}`\n    : `#${hexPair(r)}${hexPair(g)}${hexPair(b)}`\n}\n\n/** A CSS color for the swatch and the slider tracks, alpha included. */\nconst toCss = (color: Hsla) => formatColor(color, \"rgb\")\n\n/** Default copy, keyed by code so a caller can replace any single line. */\nexport const colorMessages: Record<ColorErrorCode, string> = {\n  invalid: \"Enter a color like #3b82f6, rgb(59 130 246) or hsl(217 91% 60%).\",\n  \"named-color\": \"Color names are not supported — use a hex, rgb() or hsl() value.\",\n}\n\n/** Slider and group labels, separated out so they can be translated. */\nexport const colorLabels = {\n  hue: \"Hue\",\n  saturation: \"Saturation\",\n  lightness: \"Lightness\",\n  alpha: \"Alpha\",\n  swatches: \"Preset colors\",\n}\n\nexport type ColorLabels = typeof colorLabels\n\nconst FALLBACK: Hsla = { h: 217, s: 91, l: 60, a: 1 }\n\nfunction seedColor(value: string | null | undefined): Hsla {\n  if (value === null || value === undefined) return FALLBACK\n  const parsed = parseColor(value)\n  return parsed.ok ? parsed.color : FALLBACK\n}\n\ninterface ChannelSliderProps {\n  label: string\n  value: number\n  max: number\n  step: number\n  /** Rendered into `aria-valuetext`, because \"210\" alone does not say degrees. */\n  unit: string\n  track: string\n  disabled?: boolean\n  onValueChange: (value: number) => void\n}\n\nfunction ChannelSlider({\n  label,\n  value,\n  max,\n  step,\n  unit,\n  track,\n  disabled,\n  onValueChange,\n}: ChannelSliderProps) {\n  const shown = step < 1 ? Math.round(value * 100) / 100 : Math.round(value)\n  return (\n    <input\n      type=\"range\"\n      min={0}\n      max={max}\n      step={step}\n      value={shown}\n      disabled={disabled}\n      aria-label={label}\n      aria-valuetext={`${unit === \"%\" || unit === \"°\" ? shown : shown}${unit}`}\n      onChange={(event) => onValueChange(Number(event.target.value))}\n      style={{ backgroundImage: track }}\n      className={cn(\n        \"h-3 w-full cursor-pointer appearance-none rounded-full border border-input bg-cover bg-center\",\n        \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n        \"disabled:cursor-not-allowed disabled:opacity-50\",\n        \"[&::-webkit-slider-runnable-track]:h-3 [&::-webkit-slider-runnable-track]:rounded-full\",\n        \"[&::-moz-range-track]:h-3 [&::-moz-range-track]:rounded-full [&::-moz-range-track]:bg-transparent\",\n        \"[&::-webkit-slider-thumb]:-mt-0.5 [&::-webkit-slider-thumb]:size-4 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-background [&::-webkit-slider-thumb]:bg-foreground [&::-webkit-slider-thumb]:shadow\",\n        \"[&::-moz-range-thumb]:size-4 [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-background [&::-moz-range-thumb]:bg-foreground [&::-moz-range-thumb]:shadow\"\n      )}\n    />\n  )\n}\n\nexport interface ColorPickerProps\n  extends Omit<\n    React.ComponentPropsWithoutRef<\"div\">,\n    \"onChange\" | \"defaultValue\" | \"color\"\n  > {\n  /** Controlled color, in any notation `parseColor` accepts. */\n  value?: string | null\n  /** Initial color, for uncontrolled use. Defaults to a mid blue. */\n  defaultValue?: string | null\n  /**\n   * Fires with the color written in `format`. `null` while what is typed cannot\n   * be read, so a value handed over here never has to be validated again.\n   */\n  onValueChange?: (value: string | null) => void\n  /** Notation the field normalizes to and reports in (default \"hex\"). */\n  format?: ColorFormat\n  /** Show the alpha slider and keep alpha in the output (default false). */\n  alpha?: boolean\n  /** Shortcut colors shown under the sliders. Entries that cannot be read are skipped. */\n  swatches?: string[]\n  /** Posts the color through a hidden input under this name. */\n  name?: string\n  disabled?: boolean\n  /** Show the error line under the field (default true). */\n  showHint?: boolean\n  /** Replace any default message. */\n  messages?: Partial<Record<ColorErrorCode, string>>\n  /** Replace any slider or group label. */\n  labels?: Partial<ColorLabels>\n  /** Class for the text input; `className` goes to the wrapper. */\n  inputClassName?: string\n}\n\nexport const ColorPicker = React.forwardRef<HTMLDivElement, ColorPickerProps>(\n  function ColorPicker(\n    {\n      className,\n      inputClassName,\n      value,\n      defaultValue,\n      onValueChange,\n      format = \"hex\",\n      alpha = false,\n      swatches,\n      name,\n      disabled,\n      showHint = true,\n      messages,\n      labels,\n      id,\n      ...props\n    },\n    ref\n  ) {\n    const generatedId = React.useId()\n    const inputId = id ?? generatedId\n    const hintId = `${inputId}-hint`\n\n    const isControlled = value !== undefined\n\n    // Every color arriving from outside passes through here, so the field can\n    // never end up holding an alpha it has no slider to change: without the\n    // alpha slider, a seeded or pasted `#11223344` is a color the reader can see\n    // but not edit, and the picker would go on reporting it.\n    const settle = React.useCallback(\n      (next: Hsla): Hsla => (alpha ? next : { ...next, a: 1 }),\n      [alpha]\n    )\n\n    const [color, setColor] = React.useState<Hsla>(() =>\n      settle(seedColor(isControlled ? value : defaultValue))\n    )\n    // The text is held separately from the color because an unreadable entry has\n    // to stay on screen — the reader needs to see and fix what they wrote — while\n    // the sliders go on showing the last color that could actually be read.\n    const [text, setText] = React.useState(() =>\n      formatColor(settle(seedColor(isControlled ? value : defaultValue)), format)\n    )\n\n    const parsed = parseColor(text)\n    const invalid = !parsed.ok && text.trim() !== \"\"\n    const committed = parsed.ok ? formatColor(settle(parsed.color), format) : null\n\n    // What the parent was last told. Comparison is on the written form, not the\n    // numbers, so a parent that echoes our own \"#3b82f6\" back — or writes it as\n    // \"#3B82F6\" — is recognised as an echo and does not reset the sliders.\n    const lastEmitted = React.useRef<string | null>(committed)\n\n    React.useEffect(() => {\n      if (!isControlled) return\n      const incoming = value === null || value === undefined ? null : value\n      const canonical =\n        incoming === null\n          ? null\n          : (() => {\n              const result = parseColor(incoming)\n              return result.ok ? formatColor(result.color, format) : incoming\n            })()\n      if (canonical === lastEmitted.current) return\n      lastEmitted.current = canonical\n      const next = settle(seedColor(incoming))\n      setColor(next)\n      setText(canonical === null ? \"\" : formatColor(next, format))\n    }, [isControlled, value, format, settle])\n\n    function emit(next: string | null) {\n      if (next === lastEmitted.current) return\n      lastEmitted.current = next\n      onValueChange?.(next)\n    }\n\n    // No `settle` here: a slider only ever adjusts the color already in state,\n    // which was settled on the way in, and the alpha slider is not rendered at\n    // all when alpha is off.\n    function commitColor(next: Hsla) {\n      setColor(next)\n      const written = formatColor(next, format)\n      setText(written)\n      emit(written)\n    }\n\n    function commitText(next: string) {\n      setText(next)\n      const result = parseColor(next)\n      if (!result.ok) return emit(null)\n      const settled = settle(result.color)\n      setColor(settled)\n      emit(formatColor(settled, format))\n    }\n\n    // Blur is where the reading becomes visible: \"#ABC\" is rewritten as\n    // \"#aabbcc\", \"rgb(300 0 0)\" as \"rgb(255 0 0)\", and an alpha dropped for want\n    // of the alpha slider is seen to be gone. An empty box is refilled, because\n    // the sliders beside it are still showing a color and the two should not\n    // disagree.\n    function handleBlur() {\n      if (parsed.ok || text.trim() === \"\") {\n        const written = formatColor(parsed.ok ? settle(parsed.color) : color, format)\n        if (written !== text) setText(written)\n        emit(written)\n      }\n    }\n\n    const say = (code: ColorErrorCode) => messages?.[code] ?? colorMessages[code]\n    const label = (key: keyof ColorLabels) => labels?.[key] ?? colorLabels[key]\n\n    const hint = !parsed.ok && invalid ? say(parsed.code) : \"\"\n\n    const opaque = { ...color, a: 1 }\n    const tracks = {\n      hue: `linear-gradient(to right, ${[0, 60, 120, 180, 240, 300, 360]\n        .map((h) => toCss({ ...opaque, h }))\n        .join(\", \")})`,\n      saturation: `linear-gradient(to right, ${toCss({ ...opaque, s: 0 })}, ${toCss({ ...opaque, s: 100 })})`,\n      lightness: `linear-gradient(to right, ${toCss({ ...opaque, l: 0 })}, ${toCss({ ...opaque, l: 50 })}, ${toCss({ ...opaque, l: 100 })})`,\n      alpha: `linear-gradient(to right, ${toCss({ ...color, a: 0 })}, ${toCss(opaque)})`,\n    }\n\n    const usableSwatches = (swatches ?? []).flatMap((entry) => {\n      const result = parseColor(entry)\n      return result.ok ? [{ entry, written: formatColor(result.color, format) }] : []\n    })\n\n    return (\n      <div\n        ref={ref}\n        className={cn(\"flex w-full max-w-xs flex-col gap-3\", className)}\n        {...props}\n      >\n        <div className=\"flex items-center gap-2\">\n          {/* Decorative: the value it stands for is in the text box beside it,\n              spelled out. The checker showing through a translucent color is\n              drawn from the theme's own foreground, so it follows dark mode. */}\n          <span\n            aria-hidden=\"true\"\n            className=\"relative size-9 shrink-0 overflow-hidden rounded-md border border-input text-muted-foreground/25\"\n            style={{\n              backgroundImage:\n                \"conic-gradient(from 90deg, currentColor 25%, transparent 0 50%, currentColor 0 75%, transparent 0)\",\n              backgroundSize: \"8px 8px\",\n            }}\n          >\n            <span\n              className=\"absolute inset-0\"\n              style={{ backgroundColor: toCss(color) }}\n            />\n          </span>\n          <input\n            id={inputId}\n            type=\"text\"\n            autoComplete=\"off\"\n            autoCapitalize=\"none\"\n            spellCheck={false}\n            disabled={disabled}\n            value={text}\n            onChange={(event) => commitText(event.target.value)}\n            onBlur={handleBlur}\n            aria-invalid={invalid || undefined}\n            aria-describedby={showHint ? hintId : undefined}\n            className={cn(\n              \"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 font-mono text-sm shadow-sm transition-colors\",\n              \"placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring\",\n              invalid && \"border-destructive focus-visible:ring-destructive\",\n              \"disabled:cursor-not-allowed disabled:opacity-50\",\n              inputClassName\n            )}\n          />\n        </div>\n\n        <div className=\"flex flex-col gap-2\">\n          <ChannelSlider\n            label={label(\"hue\")}\n            value={color.h}\n            max={360}\n            step={1}\n            unit=\"°\"\n            track={tracks.hue}\n            disabled={disabled}\n            onValueChange={(h) => commitColor({ ...color, h })}\n          />\n          <ChannelSlider\n            label={label(\"saturation\")}\n            value={color.s}\n            max={100}\n            step={1}\n            unit=\"%\"\n            track={tracks.saturation}\n            disabled={disabled}\n            onValueChange={(s) => commitColor({ ...color, s })}\n          />\n          <ChannelSlider\n            label={label(\"lightness\")}\n            value={color.l}\n            max={100}\n            step={1}\n            unit=\"%\"\n            track={tracks.lightness}\n            disabled={disabled}\n            onValueChange={(l) => commitColor({ ...color, l })}\n          />\n          {alpha ? (\n            <ChannelSlider\n              label={label(\"alpha\")}\n              value={color.a}\n              max={1}\n              step={0.01}\n              unit=\"\"\n              track={tracks.alpha}\n              disabled={disabled}\n              onValueChange={(a) => commitColor({ ...color, a })}\n            />\n          ) : null}\n        </div>\n\n        {usableSwatches.length > 0 ? (\n          <div role=\"group\" aria-label={label(\"swatches\")} className=\"flex flex-wrap gap-1.5\">\n            {usableSwatches.map((swatch) => {\n              const current = swatch.written === committed\n              return (\n                <button\n                  key={swatch.entry}\n                  type=\"button\"\n                  disabled={disabled}\n                  aria-label={swatch.written}\n                  // Marks which preset is showing without claiming it is a\n                  // toggle that can be pressed off again.\n                  aria-current={current || undefined}\n                  onClick={() => commitText(swatch.written)}\n                  style={{ backgroundColor: swatch.written }}\n                  className={cn(\n                    \"size-6 rounded-md border border-input shadow-sm transition-transform\",\n                    \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n                    \"disabled:cursor-not-allowed disabled:opacity-50\",\n                    current && \"ring-2 ring-ring ring-offset-2 ring-offset-background\"\n                  )}\n                />\n              )\n            })}\n          </div>\n        ) : null}\n\n        {/* A form gets the color, and gets nothing at all when what is in the\n            box cannot be read — an aria-invalid field that still posts a value\n            is the worst of both. */}\n        {name ? (\n          <input\n            type=\"hidden\"\n            name={name}\n            disabled={disabled}\n            value={committed ?? \"\"}\n          />\n        ) : null}\n\n        {showHint ? (\n          <p\n            id={hintId}\n            aria-live=\"polite\"\n            className={cn(\"min-h-4 text-xs\", invalid ? \"text-destructive\" : \"text-muted-foreground\")}\n          >\n            {hint}\n          </p>\n        ) : null}\n      </div>\n    )\n  }\n)\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}