{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "color-picker",
  "title": "颜色选择",
  "description": "可直接编辑 HEX 的方条颜色选择，支持二维取色、透明度与预设色板。",
  "dependencies": [
    "@base-ui/react",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "popover",
    "sheet",
    "https://yiui.dev/r/bar-field.json",
    "https://yiui.dev/r/color.json"
  ],
  "files": [
    {
      "path": "src/registry/ui/color-picker/color-picker.tsx",
      "content": "\"use client\"\nimport * as React from \"react\"\nimport { ColorPickerPanel } from \"./color-panel\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Popover,\n  PopoverTrigger,\n  PopoverContent,\n} from \"@/components/ui/popover\"\nimport {\n  Sheet,\n  SheetContent,\n  SheetTitle,\n  SheetDescription,\n} from \"@/components/ui/sheet\"\nimport { barFieldVariants } from \"@/components/ui/bar-field/bar-field\"\nimport { useControllableState } from \"@/hooks/use-controllable-state\"\nimport {\n  parseHex,\n  rgbToHex,\n  rgbToHsv,\n  hsvToRgb,\n  type HSV,\n} from \"@/lib/color\"\nimport {\n  ColorArea,\n  ColorHueSlider,\n  ColorAlphaSlider,\n  ColorSwatch,\n} from \"./color-parts\"\nimport { cn } from \"@/lib/utils\"\nconst defaultLabels = {\n  open: \"选择颜色\",\n  invalid: \"请输入有效的 HEX 颜色\",\n  saturation: \"饱和度\",\n  brightness: \"明度\",\n  hue: \"色相\",\n  alpha: \"透明度\",\n  format: \"颜色格式\",\n  presets: \"预设色板\",\n  eyeDropper: \"吸取屏幕颜色\",\n  done: \"完成\",\n  red: \"红\",\n  green: \"绿\",\n  blue: \"蓝\",\n  lightness: \"亮度\",\n}\ntype ColorPickerLabels = typeof defaultLabels\ntype ColorPickerProps = Omit<\n  React.ComponentProps<\"div\">,\n  \"defaultValue\" | \"onChange\"\n> & {\n  /** 方条标签；省略时提供 aria-label */ label?: React.ReactNode\n  /** 受控 HEX */ value?: string\n  /** 初始颜色 @default \"#2563eb\" */ defaultValue?: string\n  /** 变化时持续触发 */ onValueChange?: (value: string) => void\n  /** 操作结束触发 */ onValueCommit?: (value: string) => void\n  /** 输出包含透明度 @default false */ alpha?: boolean\n  /** 初始面板格式 @default \"HEX\" */ format?: \"HEX\" | \"RGB\" | \"HSL\"\n  /** 预设颜色 @default [] */ presets?: string[]\n  /** 中文文案覆盖 */ labels?: Partial<ColorPickerLabels>\n  /** 高度档 @default \"default\" */ size?: \"sm\" | \"default\" | \"lg\"\n  /** 禁用 @default false */ disabled?: boolean\n  /** 只读 @default false */ readOnly?: boolean\n  /** 错误信息 */ error?: string\n  /** 说明 */ description?: React.ReactNode\n  /** HEX 输入框的 id，方便外部 label 关联 */ id?: string\n  /** 表单字段名 */ name?: string\n  /** 所属表单 */ form?: string\n}\nfunction subscribe(notify: () => void) {\n  const m = matchMedia(\"(pointer: coarse)\")\n  m.addEventListener(\"change\", notify)\n  return () => m.removeEventListener(\"change\", notify)\n}\nfunction ColorPicker({\n  id: idProp,\n  label,\n  value: prop,\n  defaultValue = \"#2563eb\",\n  onValueChange,\n  onValueCommit,\n  alpha = false,\n  format: initialFormat = \"HEX\",\n  presets = [],\n  labels: overrides,\n  size = \"default\",\n  disabled,\n  readOnly,\n  error,\n  description,\n  name,\n  form,\n  className,\n  ...props\n}: ColorPickerProps) {\n  const autoId = React.useId()\n  const id = idProp ?? autoId\n  const labels = { ...defaultLabels, ...overrides }\n  const [value, setValue] = useControllableState({\n    prop,\n    defaultProp: defaultValue,\n    onChange: onValueChange,\n  })\n  const [local, setLocal] = React.useState<HSV>(() =>\n    rgbToHsv(parseHex(value) ?? parseHex(\"#2563eb\")!),\n  )\n  const parsed = rgbToHsv(parseHex(value) ?? parseHex(\"#2563eb\")!, local.h)\n  const renderedHex = rgbToHex(hsvToRgb(local), alpha)\n  const hsv =\n    rgbToHex(parseHex(value) ?? hsvToRgb(local), alpha) === renderedHex\n      ? local\n      : parsed\n  const [draft, setDraft] = React.useState<string | null>(null)\n  const [invalid, setInvalid] = React.useState(false)\n  const [format, setFormat] = React.useState(initialFormat)\n  const [open, setOpen] = React.useState(false),\n    [sheetOpen, setSheetOpen] = React.useState(false)\n  const coarse = React.useSyncExternalStore(\n    subscribe,\n    () => matchMedia(\"(pointer: coarse)\").matches,\n    () => false,\n  )\n  const eye = React.useSyncExternalStore(\n    () => () => {},\n    () => \"EyeDropper\" in window,\n    () => false,\n  )\n  const locked = !!(disabled || readOnly)\n  const lockedRef = React.useRef(locked)\n  React.useLayoutEffect(() => {\n    lockedRef.current = locked\n  }, [locked])\n  const [wasLocked, setWasLocked] = React.useState(locked)\n  if (wasLocked !== locked) {\n    setWasLocked(locked)\n    if (locked) {\n      setOpen(false)\n      setSheetOpen(false)\n      setDraft(null)\n      setInvalid(false)\n    }\n  }\n  // 面板关闭时丢弃无效草稿，回到当前颜色；有效草稿仍由失焦提交\n  const panelOpen = coarse ? sheetOpen : open\n  const [wasPanelOpen, setWasPanelOpen] = React.useState(panelOpen)\n  if (wasPanelOpen !== panelOpen) {\n    setWasPanelOpen(panelOpen)\n    if (!panelOpen) {\n      if (draft !== null && !parseHex(draft, { alpha })) setDraft(null)\n      setInvalid(false)\n    }\n  }\n  function commit(hex: string) {\n    if (!lockedRef.current) onValueCommit?.(hex)\n  }\n  const root = React.useRef<HTMLDivElement>(null)\n  function change(next: HSV, shouldCommit = false) {\n    if (lockedRef.current) return\n    setLocal(next)\n    const hex = rgbToHex(hsvToRgb(next), alpha)\n    setValue(hex)\n    setDraft(null)\n    setInvalid(false)\n    if (shouldCommit) onValueCommit?.(hex)\n  }\n  function submit(text: string) {\n    if (lockedRef.current) return\n    const rgb = parseHex(text, { alpha })\n    if (!rgb) {\n      setInvalid(true)\n      return\n    }\n    change(rgbToHsv(rgb, local.h), true)\n  }\n  React.useEffect(() => {\n    const el =\n      root.current?.closest(\"form\") ??\n      (form ? document.getElementById(form) : null)\n    const reset = () => {\n      if (prop === undefined) {\n        const next = rgbToHsv(parseHex(defaultValue)!)\n        setLocal(next)\n        setValue(rgbToHex(hsvToRgb(next), alpha))\n        setDraft(null)\n        setInvalid(false)\n      }\n    }\n    el?.addEventListener(\"reset\", reset)\n    return () => el?.removeEventListener(\"reset\", reset)\n  }, [prop, defaultValue, form, setValue, alpha])\n  const hex = rgbToHex(hsvToRgb(hsv), alpha)\n  const panel = (\n    <ColorPickerPanel\n      {...{\n        presets,\n        hsv,\n        hex,\n        alpha,\n        labels,\n        format,\n        setFormat,\n        draft,\n        setDraft,\n        invalid,\n        submit,\n        change,\n        onValueCommit: commit,\n        locked,\n        eye,\n        coarse,\n        setSheetOpen,\n      }}\n    />\n  )\n  const trigger = (\n    <Button\n      type=\"button\"\n      size={size === \"sm\" ? \"icon-sm\" : size === \"lg\" ? \"icon-lg\" : \"icon\"}\n      variant=\"ghost\"\n      disabled={disabled || readOnly}\n      // 同一页常有多个颜色选择，按钮名带上字段名才分得清\n      aria-label={\n        typeof label === \"string\" ? `${labels.open}：${label}` : labels.open\n      }\n      aria-haspopup=\"dialog\"\n      aria-expanded={coarse ? sheetOpen : open}\n      className=\"shrink-0 focus-visible:ring-inset\"\n      onClick={() => {\n        if (coarse) setSheetOpen(true)\n      }}\n    >\n      <ColorSwatch color={hex} />\n    </Button>\n  )\n  return (\n    <div\n      {...props}\n      ref={root}\n      data-slot=\"color-picker\"\n      className={cn(\n        \"w-full min-w-0 in-data-[slot=grid-toolbar]:h-full\",\n        className,\n      )}\n    >\n      <Popover\n        open={!locked && !coarse && open}\n        onOpenChange={(next) => setOpen(next && !lockedRef.current)}\n      >\n        <div className=\"@container/color-picker flex h-full flex-col gap-1.5\">\n          {label && (\n            <label\n              htmlFor={id}\n              aria-hidden\n              className=\"hidden text-xs text-muted-foreground in-data-[slot=grid-toolbar]:hidden! @max-[20rem]/color-picker:block\"\n            >\n              {label}\n            </label>\n          )}\n          <div\n            data-slot=\"color-picker-control\"\n            className={cn(\n              barFieldVariants({ size }),\n              // 与 bar-field 同一套状态样式\n              invalid || error\n                ? \"border-destructive ring-3 ring-destructive/20 dark:border-destructive/50 dark:ring-destructive/40\"\n                : \"border-edge focus-within:border-foreground/25 focus-within:bg-background hover:not-focus-within:bg-muted dark:focus-within:bg-input/30\",\n              disabled && \"opacity-50\",\n              // 面板打开时方条保持高亮：鼠标移进面板后，仍看得出面板属于这一条\n              panelOpen && \"bg-muted in-data-[slot=grid-toolbar]:bg-muted\",\n            )}\n          >\n            {label && (\n              <label\n                htmlFor={id}\n                // mr-auto 把色块与 HEX 推到右侧，与方条文本框「标签靠左、值靠右」一致\n                className=\"mr-auto shrink-0 text-muted-foreground in-data-[slot=grid-toolbar]:not-sr-only! @max-[20rem]/color-picker:sr-only\"\n              >\n                {label}\n              </label>\n            )}\n            {coarse ? trigger : <PopoverTrigger render={trigger} />}\n            <input\n              id={id}\n              disabled={disabled}\n              readOnly={readOnly}\n              aria-label={props[\"aria-label\"]}\n              aria-labelledby={props[\"aria-labelledby\"]}\n              aria-invalid={!!(invalid || error)}\n              aria-describedby={\n                invalid || error || description\n                  ? `${id}-description`\n                  : undefined\n              }\n              className={cn(\n                \"h-full min-w-0 bg-transparent font-mono text-base outline-none pointer-fine:md:text-sm\",\n                // 有标签时按 HEX 位数定宽（等宽字体），整组靠右；无标签或标签移到上方时占满剩余宽度\n                label\n                  ? cn(\n                      alpha ? \"w-[10ch]\" : \"w-[8ch]\",\n                      \"@max-[20rem]/color-picker:flex-1\",\n                    )\n                  : \"flex-1\",\n              )}\n              value={draft ?? hex}\n              onChange={(e) => {\n                setDraft(e.target.value)\n                setInvalid(false)\n              }}\n              onBlur={() => {\n                if (draft !== null) submit(draft)\n              }}\n              onKeyDown={(e) => {\n                if (e.key === \"Enter\") {\n                  e.preventDefault()\n                  submit(draft ?? hex)\n                }\n              }}\n            />\n          </div>\n          {(invalid || error || description) && (\n            <p\n              id={`${id}-description`}\n              className={cn(\n                \"text-xs\",\n                invalid || error ? \"text-destructive\" : \"text-muted-foreground\",\n              )}\n            >\n              {invalid ? labels.invalid : (error ?? description)}\n            </p>\n          )}\n        </div>\n        {!coarse && (\n          <PopoverContent\n            aria-label={labels.open}\n            className=\"w-72 max-w-[calc(100vw-2rem)]\"\n          >\n            {panel}\n          </PopoverContent>\n        )}\n      </Popover>\n      {coarse && (\n        <Sheet\n          open={!locked && sheetOpen}\n          onOpenChange={(next) => setSheetOpen(next && !lockedRef.current)}\n        >\n          <SheetContent\n            data-slot=\"color-picker-sheet\"\n            side=\"bottom\"\n            showCloseButton={false}\n            className=\"max-h-[90dvh] overflow-y-auto p-4 pb-[max(1rem,env(safe-area-inset-bottom))]\"\n          >\n            <SheetTitle>{label ?? labels.open}</SheetTitle>\n            <SheetDescription className=\"sr-only\">\n              {labels.open}\n            </SheetDescription>\n            {panel}\n          </SheetContent>\n        </Sheet>\n      )}\n      {name && (\n        <input\n          type=\"hidden\"\n          name={name}\n          form={form}\n          value={hex}\n          disabled={disabled}\n        />\n      )}\n    </div>\n  )\n}\nexport { ColorPicker, ColorArea, ColorHueSlider, ColorAlphaSlider, ColorSwatch }\nexport type { ColorPickerProps, ColorPickerLabels }\n",
      "type": "registry:ui",
      "target": "@ui/color-picker/color-picker.tsx"
    },
    {
      "path": "src/registry/ui/color-picker/color-parts.tsx",
      "content": "\"use client\"\nimport * as React from \"react\"\nimport { Slider } from \"@base-ui/react/slider\"\nimport { cn } from \"@/lib/utils\"\nimport { clamp, hsvToRgb, rgbToHex, type HSV } from \"@/lib/color\"\n\nconst checker: React.CSSProperties = {\n  backgroundColor: \"var(--background)\",\n  backgroundImage:\n    \"conic-gradient(var(--muted) 25%, transparent 0 50%, var(--muted) 0 75%, transparent 0)\",\n  backgroundSize: \"12px 12px\",\n}\nfunction ColorSwatch({\n  color,\n  className,\n  style,\n  ...props\n}: React.ComponentProps<\"span\"> & { color: string }) {\n  return (\n    <span\n      data-slot=\"color-swatch\"\n      className={cn(\n        \"relative inline-block size-5 shrink-0 rounded-sm\",\n        className,\n      )}\n      style={{ ...checker, ...style }}\n      {...props}\n    >\n      {/* 内描边画在颜色层之上：深色在暗色背景、浅色在亮色背景上都能看清色块边界 */}\n      <span\n        className=\"absolute inset-0 rounded-[inherit] ring-1 ring-foreground/20 ring-inset\"\n        style={{ background: color }}\n      />\n    </span>\n  )\n}\ntype AreaProps = Omit<React.ComponentProps<\"div\">, \"onChange\"> & {\n  /** HSV 颜色 */ value: HSV\n  /** 拖动及键盘变化 */ onValueChange: (value: HSV) => void\n  /** 松手或按键结束 */ onValueCommit?: (value: HSV) => void\n  /** 读屏名称 @default 中文 */ labels?: {\n    saturation?: string\n    brightness?: string\n  }\n}\nfunction ColorArea({\n  value,\n  onValueChange,\n  onValueCommit,\n  labels,\n  className,\n  ...props\n}: AreaProps) {\n  const latest = React.useRef(value)\n  React.useEffect(() => {\n    latest.current = value\n  }, [value])\n  const frame = React.useRef<number>(0)\n  React.useEffect(() => () => cancelAnimationFrame(frame.current), [])\n  function update(e: React.PointerEvent<HTMLDivElement>) {\n    const rect = e.currentTarget.getBoundingClientRect()\n    const next = {\n      ...latest.current,\n      s: clamp((e.clientX - rect.left) / rect.width),\n      v: 1 - clamp((e.clientY - rect.top) / rect.height),\n    }\n    latest.current = next\n    cancelAnimationFrame(frame.current)\n    frame.current = requestAnimationFrame(() => onValueChange(next))\n  }\n  return (\n    <div\n      data-slot=\"color-area\"\n      className={cn(\n        \"relative h-44 touch-none rounded-md select-none has-focus-visible:ring-3 has-focus-visible:ring-ring/50\",\n        className,\n      )}\n      style={{\n        background: `linear-gradient(to top, #000, transparent), linear-gradient(to right, #fff, transparent), hsl(${value.h} 100% 50%)`,\n      }}\n      {...props}\n      onPointerDown={(e) => {\n        if (e.button !== 0) return\n        e.preventDefault()\n        e.currentTarget.setPointerCapture(e.pointerId)\n        update(e)\n      }}\n      onPointerMove={(e) => {\n        if (e.currentTarget.hasPointerCapture(e.pointerId)) update(e)\n      }}\n      onPointerUp={(e) => {\n        if (!e.currentTarget.hasPointerCapture(e.pointerId)) return\n        update(e)\n        cancelAnimationFrame(frame.current)\n        onValueChange(latest.current)\n        onValueCommit?.(latest.current)\n        e.currentTarget.releasePointerCapture(e.pointerId)\n      }}\n      onPointerCancel={() => {\n        cancelAnimationFrame(frame.current)\n        onValueCommit?.(latest.current)\n      }}\n    >\n      <span\n        aria-hidden\n        className=\"pointer-events-none absolute size-3 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-background shadow-sm ring-1 ring-foreground\"\n        style={{ left: `${value.s * 100}%`, top: `${(1 - value.v) * 100}%` }}\n      />\n      {([\"s\", \"v\"] as const).map((key) => (\n        <input\n          key={key}\n          type=\"range\"\n          className=\"sr-only\"\n          min={0}\n          max={100}\n          step={1}\n          value={value[key] * 100}\n          aria-label={\n            key === \"s\"\n              ? (labels?.saturation ?? \"饱和度\")\n              : (labels?.brightness ?? \"明度\")\n          }\n          aria-valuetext={`${Math.round(value[key] * 100)}%`}\n          onChange={(e) =>\n            onValueChange({ ...value, [key]: Number(e.target.value) / 100 })\n          }\n          onKeyDown={(e) => {\n            if (\n              ![\n                \"ArrowLeft\",\n                \"ArrowRight\",\n                \"ArrowUp\",\n                \"ArrowDown\",\n                \"Home\",\n                \"End\",\n              ].includes(e.key)\n            )\n              return\n            e.preventDefault()\n            const n =\n              e.key === \"Home\"\n                ? 0\n                : e.key === \"End\"\n                  ? 1\n                  : clamp(\n                      value[key] +\n                        ([\"ArrowLeft\", \"ArrowDown\"].includes(e.key) ? -1 : 1) *\n                          (e.shiftKey ? 0.1 : 0.01),\n                    )\n            onValueChange({ ...value, [key]: n })\n          }}\n          onKeyUp={() => onValueCommit?.(latest.current)}\n        />\n      ))}\n    </div>\n  )\n}\ntype ColorSliderProps = {\n  /** 当前值 */ value: number\n  /** 变化 */ onValueChange: (value: number) => void\n  /** 提交 */ onValueCommit?: (value: number) => void\n  /** 读屏名称 */ label?: string\n  className?: string\n}\nfunction ColorTrack({\n  value,\n  onValueChange,\n  onValueCommit,\n  label,\n  max,\n  background,\n  className,\n}: ColorSliderProps & { max: number; background: string }) {\n  return (\n    <Slider.Root\n      data-slot=\"color-track\"\n      min={0}\n      max={max}\n      step={max === 1 ? 0.01 : 1}\n      value={value}\n      onValueChange={(v) => onValueChange(v as number)}\n      onValueCommitted={(v) => onValueCommit?.(v as number)}\n      className={cn(\"w-full\", className)}\n    >\n      <Slider.Control className=\"flex h-7 items-center pointer-coarse:h-11\">\n        <Slider.Track\n          className=\"relative h-3 w-full rounded-full\"\n          style={{\n            ...checker,\n            backgroundImage: background,\n            backgroundSize: \"100% 100%, 12px 12px\",\n          }}\n        >\n          <Slider.Thumb\n            aria-label={label}\n            getAriaValueText={(v) =>\n              `${label} ${Math.round(Number(v) * (max === 1 ? 100 : 1))}${max === 1 ? \"%\" : \" 度\"}`\n            }\n            className=\"size-4 rounded-full border-2 border-background bg-foreground shadow-sm outline-none focus-visible:ring-3 focus-visible:ring-ring/50\"\n          />\n        </Slider.Track>\n      </Slider.Control>\n    </Slider.Root>\n  )\n}\nfunction ColorHueSlider(props: ColorSliderProps) {\n  return (\n    <ColorTrack\n      {...props}\n      label={props.label ?? \"色相\"}\n      max={360}\n      background=\"linear-gradient(to right, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00)\"\n    />\n  )\n}\nfunction ColorAlphaSlider({\n  color,\n  ...props\n}: ColorSliderProps & { color: HSV }) {\n  return (\n    <ColorTrack\n      {...props}\n      label={props.label ?? \"透明度\"}\n      max={1}\n      background={`linear-gradient(to right, transparent, ${rgbToHex(hsvToRgb({ ...color, a: 1 }))}), conic-gradient(var(--muted) 25%, transparent 0 50%, var(--muted) 0 75%, transparent 0)`}\n    />\n  )\n}\nexport { ColorArea, ColorHueSlider, ColorAlphaSlider, ColorSwatch }\nexport type { AreaProps, ColorSliderProps }\n",
      "type": "registry:ui",
      "target": "@ui/color-picker/color-parts.tsx"
    },
    {
      "path": "src/registry/hooks/use-controllable-state.ts",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\ntype UseControllableStateParams<T> = {\n  /** 受控值；传入后组件不再维护内部状态 */\n  prop?: T\n  /** 非受控时的初始值 */\n  defaultProp: T\n  /** 值变化时回调（受控与非受控都会触发） */\n  onChange?: (value: T) => void\n}\n\n/**\n * 同时支持受控与非受控的状态 hook。\n * 约定见 component-conventions 技能第 4 节：open/defaultOpen/onOpenChange 等成对 props 都用它实现。\n */\nfunction useControllableState<T>({\n  prop,\n  defaultProp,\n  onChange,\n}: UseControllableStateParams<T>) {\n  const [uncontrolled, setUncontrolled] = React.useState(defaultProp)\n  const isControlled = prop !== undefined\n  const value = isControlled ? prop : uncontrolled\n\n  // 用 ref 保存最新的回调和值，保证 setValue 引用稳定\n  const onChangeRef = React.useRef(onChange)\n  const valueRef = React.useRef(value)\n  React.useEffect(() => {\n    onChangeRef.current = onChange\n    valueRef.current = value\n  })\n\n  const setValue = React.useCallback(\n    (next: React.SetStateAction<T>) => {\n      const resolved =\n        typeof next === \"function\"\n          ? (next as (prev: T) => T)(valueRef.current)\n          : next\n      if (Object.is(resolved, valueRef.current)) return\n      valueRef.current = resolved\n      if (!isControlled) setUncontrolled(resolved)\n      onChangeRef.current?.(resolved)\n    },\n    [isControlled],\n  )\n\n  return [value, setValue] as const\n}\n\nexport { useControllableState }\n",
      "type": "registry:ui",
      "target": "hooks/use-controllable-state.ts"
    },
    {
      "path": "src/registry/ui/color-picker/color-panel.tsx",
      "content": "\"use client\"\nimport { Pipette } from \"lucide-react\"\nimport { Button } from \"@/components/ui/button\"\nimport { BarField } from \"@/components/ui/bar-field/bar-field\"\nimport {\n  parseHex,\n  hsvToRgb,\n  hsvToHsl,\n  hslToHsv,\n  rgbToHsv,\n  type HSV,\n} from \"@/lib/color\"\nimport {\n  ColorArea,\n  ColorHueSlider,\n  ColorAlphaSlider,\n  ColorSwatch,\n} from \"./color-parts\"\nimport type { ColorPickerLabels } from \"./color-picker\"\ntype PanelProps = {\n  locked: boolean\n  presets: string[]\n  hsv: HSV\n  hex: string\n  alpha: boolean\n  labels: ColorPickerLabels\n  format: \"HEX\" | \"RGB\" | \"HSL\"\n  setFormat: (value: \"HEX\" | \"RGB\" | \"HSL\") => void\n  draft: string | null\n  setDraft: (value: string) => void\n  invalid: boolean\n  submit: (value: string) => void\n  change: (value: HSV, commit?: boolean) => void\n  onValueCommit?: (value: string) => void\n  eye: boolean\n  coarse: boolean\n  setSheetOpen: (value: boolean) => void\n}\nfunction ColorPickerPanel({\n  locked,\n  presets,\n  hsv,\n  hex,\n  alpha,\n  labels,\n  format,\n  setFormat,\n  draft,\n  setDraft,\n  invalid,\n  submit,\n  change,\n  onValueCommit,\n  eye,\n  coarse,\n  setSheetOpen,\n}: PanelProps) {\n  const rgb = hsvToRgb(hsv),\n    hsl = hsvToHsl(hsv)\n  const channels =\n    format === \"RGB\"\n      ? [\n          { label: labels.red, key: \"r\", value: rgb.r },\n          { label: labels.green, key: \"g\", value: rgb.g },\n          { label: labels.blue, key: \"b\", value: rgb.b },\n        ]\n      : [\n          { label: labels.hue, key: \"h\", value: hsl.h },\n          { label: labels.saturation, key: \"s\", value: hsl.s * 100 },\n          { label: labels.lightness, key: \"l\", value: hsl.l * 100 },\n        ]\n  return (\n    <div\n      inert={locked}\n      data-slot=\"color-picker-panel\"\n      className=\"flex flex-col gap-2 p-2\"\n    >\n      <ColorArea\n        value={hsv}\n        onValueChange={change}\n        onValueCommit={(v) => change(v, true)}\n        labels={labels}\n      />\n      <ColorHueSlider\n        value={hsv.h}\n        label={labels.hue}\n        onValueChange={(h) => change({ ...hsv, h })}\n        onValueCommit={(h) => change({ ...hsv, h }, true)}\n      />\n      {alpha && (\n        <ColorAlphaSlider\n          color={hsv}\n          value={hsv.a}\n          label={labels.alpha}\n          onValueChange={(a) => change({ ...hsv, a })}\n          onValueCommit={(a) => change({ ...hsv, a }, true)}\n        />\n      )}\n      <div className=\"flex gap-1\" role=\"group\" aria-label={labels.format}>\n        {([\"HEX\", \"RGB\", \"HSL\"] as const).map((f) => (\n          <Button\n            key={f}\n            type=\"button\"\n            size=\"lg\"\n            variant={format === f ? \"secondary\" : \"ghost\"}\n            aria-pressed={format === f}\n            onClick={() => setFormat(f)}\n          >\n            {f}\n          </Button>\n        ))}\n      </div>\n      {format === \"HEX\" ? (\n        <BarField\n          aria-label=\"HEX\"\n          value={draft ?? hex}\n          onValueChange={setDraft}\n          error={invalid ? labels.invalid : undefined}\n          onBlur={() => {\n            if (draft !== null) submit(draft)\n          }}\n          onKeyDown={(e) => {\n            if (e.key === \"Enter\") {\n              e.preventDefault()\n              submit(draft ?? hex)\n            }\n          }}\n        />\n      ) : (\n        <div className=\"grid grid-cols-3 gap-2\">\n          {channels.map(({ label: c, key, value }, i) => (\n            <BarField\n              key={`${format}-${c}`}\n              label={c}\n              type=\"number\"\n              min={0}\n              max={format === \"RGB\" ? 255 : i === 0 ? 360 : 100}\n              value={String(Math.round(value))}\n              onValueChange={(text) => {\n                const n = Number(text)\n                if (!Number.isFinite(n)) return\n                if (format === \"RGB\") {\n                  const next = {\n                    ...rgb,\n                    [key]: Math.min(255, Math.max(0, n)),\n                  }\n                  change(rgbToHsv(next, hsv.h))\n                } else {\n                  change(\n                    hslToHsv({\n                      ...hsl,\n                      [key]:\n                        Math.min(i === 0 ? 360 : 100, Math.max(0, n)) /\n                        (i === 0 ? 1 : 100),\n                    }),\n                  )\n                }\n              }}\n              onBlur={() => onValueCommit?.(hex)}\n              onKeyDown={(e) => {\n                if (e.key === \"Enter\") {\n                  e.preventDefault()\n                  onValueCommit?.(hex)\n                }\n              }}\n            />\n          ))}\n        </div>\n      )}\n      {presets.length > 0 && (\n        <div\n          role=\"group\"\n          aria-label={labels.presets}\n          className=\"flex flex-wrap gap-1\"\n        >\n          {presets\n            .filter((c) => parseHex(c, { alpha }))\n            .map((c) => (\n              <Button\n                key={c}\n                type=\"button\"\n                variant=\"ghost\"\n                size=\"icon-lg\"\n                aria-label={c}\n                onClick={() => submit(c)}\n              >\n                <ColorSwatch color={c} />\n              </Button>\n            ))}\n        </div>\n      )}\n      {eye && (\n        <Button\n          type=\"button\"\n          variant=\"outline\"\n          size=\"lg\"\n          onClick={() => {\n            const Constructor = (\n              window as unknown as {\n                EyeDropper: new () => {\n                  open: () => Promise<{ sRGBHex: string }>\n                }\n              }\n            ).EyeDropper\n            new Constructor()\n              .open()\n              .then((result) => submit(result.sRGBHex))\n              .catch(() => {})\n          }}\n        >\n          <Pipette aria-hidden />\n          {labels.eyeDropper}\n        </Button>\n      )}\n      {coarse && (\n        <Button type=\"button\" size=\"lg\" onClick={() => setSheetOpen(false)}>\n          {labels.done}\n        </Button>\n      )}\n    </div>\n  )\n}\nexport { ColorPickerPanel }\n",
      "type": "registry:ui",
      "target": "@ui/color-picker/color-panel.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-edge": "var(--edge)"
    },
    "light": {
      "edge": "color-mix(in oklch, var(--foreground) 12%, transparent)"
    },
    "dark": {
      "edge": "color-mix(in oklch, var(--foreground) 18%, transparent)"
    }
  },
  "type": "registry:ui"
}
