{
  "name": "color-picker",
  "type": "registry:ui",
  "dependencies": [
    "cn",
    "radix-ui"
  ],
  "registryDependencies": [
    "button",
    "input",
    "popover",
    "select",
    "@diceui/use-as-ref",
    "@diceui/use-isomorphic-layout-effect",
    "@diceui/use-lazy-ref"
  ],
  "files": [
    {
      "path": "ui/color-picker.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"cn\";\nimport {\n  Direction as DirectionPrimitive,\n  Slider as SliderPrimitive,\n  Slot as SlotPrimitive,\n} from \"radix-ui\";\nimport * as React from \"react\";\n\nimport { useComposedRefs } from \"@/lib/compose-refs\";\nimport { VisuallyHiddenInput } from \"@/registry/bases/radix/components/visually-hidden-input\";\nimport { useAsRef } from \"@/registry/bases/radix/hooks/use-as-ref\";\nimport { useIsomorphicLayoutEffect } from \"@/registry/bases/radix/hooks/use-isomorphic-layout-effect\";\nimport { useLazyRef } from \"@/registry/bases/radix/hooks/use-lazy-ref\";\nimport { Button } from \"@/registry/bases/radix/ui/button\";\nimport { Input } from \"@/registry/bases/radix/ui/input\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/registry/bases/radix/ui/popover\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/registry/bases/radix/ui/select\";\nimport { IconPlaceholder } from \"@/registry/icons/icon-placeholder\";\n\nconst ROOT_NAME = \"ColorPicker\";\nconst ROOT_IMPL_NAME = \"ColorPickerImpl\";\nconst TRIGGER_NAME = \"ColorPickerTrigger\";\nconst CONTENT_NAME = \"ColorPickerContent\";\nconst AREA_NAME = \"ColorPickerArea\";\nconst HUE_SLIDER_NAME = \"ColorPickerHueSlider\";\nconst ALPHA_SLIDER_NAME = \"ColorPickerAlphaSlider\";\nconst SWATCH_NAME = \"ColorPickerSwatch\";\nconst EYE_DROPPER_NAME = \"ColorPickerEyeDropper\";\nconst FORMAT_SELECT_NAME = \"ColorPickerFormatSelect\";\nconst INPUT_NAME = \"ColorPickerInput\";\n\nconst colorFormats = [\"hex\", \"rgb\", \"hsl\", \"hsb\"] as const;\n\ninterface DivProps extends React.ComponentProps<\"div\"> {\n  asChild?: boolean;\n}\n\ntype RootElement = React.ComponentRef<typeof ColorPicker>;\ntype AreaElement = React.ComponentRef<typeof ColorPickerArea>;\ntype InputElement = React.ComponentRef<typeof ColorPickerInput>;\n\ntype ColorFormat = (typeof colorFormats)[number];\n\n/**\n * @see https://gist.github.com/bkrmendy/f4582173f50fab209ddfef1377ab31e3\n */\ninterface EyeDropper {\n  open: (options?: { signal?: AbortSignal }) => Promise<{ sRGBHex: string }>;\n}\n\ndeclare global {\n  interface Window {\n    EyeDropper?: {\n      new (): EyeDropper;\n    };\n  }\n}\n\ninterface ColorValue {\n  r: number;\n  g: number;\n  b: number;\n  a: number;\n}\n\ninterface HSVColorValue {\n  h: number;\n  s: number;\n  v: number;\n  a: number;\n}\n\nfunction hexToRgb(hex: string, alpha?: number): ColorValue {\n  const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n  return result\n    ? {\n        r: Number.parseInt(result[1] ?? \"0\", 16),\n        g: Number.parseInt(result[2] ?? \"0\", 16),\n        b: Number.parseInt(result[3] ?? \"0\", 16),\n        a: alpha ?? 1,\n      }\n    : { r: 0, g: 0, b: 0, a: alpha ?? 1 };\n}\n\nfunction rgbToHex(color: ColorValue): string {\n  const toHex = (n: number) => {\n    const hex = Math.round(n).toString(16);\n    return hex.length === 1 ? `0${hex}` : hex;\n  };\n  return `#${toHex(color.r)}${toHex(color.g)}${toHex(color.b)}`;\n}\n\nfunction rgbToHsv(color: ColorValue): HSVColorValue {\n  const r = color.r / 255;\n  const g = color.g / 255;\n  const b = color.b / 255;\n\n  const max = Math.max(r, g, b);\n  const min = Math.min(r, g, b);\n  const diff = max - min;\n\n  let h = 0;\n  if (diff !== 0) {\n    switch (max) {\n      case r:\n        h = ((g - b) / diff) % 6;\n        break;\n      case g:\n        h = (b - r) / diff + 2;\n        break;\n      case b:\n        h = (r - g) / diff + 4;\n        break;\n    }\n  }\n  h = Math.round(h * 60);\n  if (h < 0) h += 360;\n\n  const s = max === 0 ? 0 : diff / max;\n  const v = max;\n\n  return {\n    h,\n    s: Math.round(s * 100),\n    v: Math.round(v * 100),\n    a: color.a,\n  };\n}\n\nfunction hsvToRgb(hsv: HSVColorValue): ColorValue {\n  const h = hsv.h / 360;\n  const s = hsv.s / 100;\n  const v = hsv.v / 100;\n\n  const i = Math.floor(h * 6);\n  const f = h * 6 - i;\n  const p = v * (1 - s);\n  const q = v * (1 - f * s);\n  const t = v * (1 - (1 - f) * s);\n\n  let r: number;\n  let g: number;\n  let b: number;\n\n  switch (i % 6) {\n    case 0: {\n      r = v;\n      g = t;\n      b = p;\n      break;\n    }\n    case 1: {\n      r = q;\n      g = v;\n      b = p;\n      break;\n    }\n    case 2: {\n      r = p;\n      g = v;\n      b = t;\n      break;\n    }\n    case 3: {\n      r = p;\n      g = q;\n      b = v;\n      break;\n    }\n    case 4: {\n      r = t;\n      g = p;\n      b = v;\n      break;\n    }\n    case 5: {\n      r = v;\n      g = p;\n      b = q;\n      break;\n    }\n    default: {\n      r = 0;\n      g = 0;\n      b = 0;\n    }\n  }\n\n  return {\n    r: Math.round(r * 255),\n    g: Math.round(g * 255),\n    b: Math.round(b * 255),\n    a: hsv.a,\n  };\n}\n\nfunction colorToString(color: ColorValue, format: ColorFormat = \"hex\"): string {\n  switch (format) {\n    case \"hex\":\n      return rgbToHex(color);\n    case \"rgb\":\n      return color.a < 1\n        ? `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})`\n        : `rgb(${color.r}, ${color.g}, ${color.b})`;\n    case \"hsl\": {\n      const hsl = rgbToHsl(color);\n      return color.a < 1\n        ? `hsla(${hsl.h}, ${hsl.s}%, ${hsl.l}%, ${color.a})`\n        : `hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)`;\n    }\n    case \"hsb\": {\n      const hsv = rgbToHsv(color);\n      return color.a < 1\n        ? `hsba(${hsv.h}, ${hsv.s}%, ${hsv.v}%, ${color.a})`\n        : `hsb(${hsv.h}, ${hsv.s}%, ${hsv.v}%)`;\n    }\n    default:\n      return rgbToHex(color);\n  }\n}\n\nfunction rgbToHsl(color: ColorValue) {\n  const r = color.r / 255;\n  const g = color.g / 255;\n  const b = color.b / 255;\n\n  const max = Math.max(r, g, b);\n  const min = Math.min(r, g, b);\n  const diff = max - min;\n  const sum = max + min;\n\n  const l = sum / 2;\n\n  let h = 0;\n  let s = 0;\n\n  if (diff !== 0) {\n    s = l > 0.5 ? diff / (2 - sum) : diff / sum;\n\n    if (max === r) {\n      h = (g - b) / diff + (g < b ? 6 : 0);\n    } else if (max === g) {\n      h = (b - r) / diff + 2;\n    } else if (max === b) {\n      h = (r - g) / diff + 4;\n    }\n    h /= 6;\n  }\n\n  return {\n    h: Math.round(h * 360),\n    s: Math.round(s * 100),\n    l: Math.round(l * 100),\n  };\n}\n\nfunction hslToRgb(\n  hsl: { h: number; s: number; l: number },\n  alpha = 1,\n): ColorValue {\n  const h = hsl.h / 360;\n  const s = hsl.s / 100;\n  const l = hsl.l / 100;\n\n  const c = (1 - Math.abs(2 * l - 1)) * s;\n  const x = c * (1 - Math.abs(((h * 6) % 2) - 1));\n  const m = l - c / 2;\n\n  let r = 0;\n  let g = 0;\n  let b = 0;\n\n  if (h >= 0 && h < 1 / 6) {\n    r = c;\n    g = x;\n    b = 0;\n  } else if (h >= 1 / 6 && h < 2 / 6) {\n    r = x;\n    g = c;\n    b = 0;\n  } else if (h >= 2 / 6 && h < 3 / 6) {\n    r = 0;\n    g = c;\n    b = x;\n  } else if (h >= 3 / 6 && h < 4 / 6) {\n    r = 0;\n    g = x;\n    b = c;\n  } else if (h >= 4 / 6 && h < 5 / 6) {\n    r = x;\n    g = 0;\n    b = c;\n  } else if (h >= 5 / 6 && h < 1) {\n    r = c;\n    g = 0;\n    b = x;\n  }\n\n  return {\n    r: Math.round((r + m) * 255),\n    g: Math.round((g + m) * 255),\n    b: Math.round((b + m) * 255),\n    a: alpha,\n  };\n}\n\nfunction parseColorString(value: string): ColorValue | null {\n  const trimmed = value.trim();\n\n  // Parse hex colors\n  if (trimmed.startsWith(\"#\")) {\n    const hexMatch = trimmed.match(/^#([a-fA-F0-9]{3}|[a-fA-F0-9]{6})$/);\n    if (hexMatch) {\n      return hexToRgb(trimmed);\n    }\n  }\n\n  // Parse rgb/rgba colors\n  const rgbMatch = trimmed.match(\n    /^rgba?\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*(?:,\\s*([\\d.]+))?\\s*\\)$/,\n  );\n  if (rgbMatch) {\n    return {\n      r: Number.parseInt(rgbMatch[1] ?? \"0\", 10),\n      g: Number.parseInt(rgbMatch[2] ?? \"0\", 10),\n      b: Number.parseInt(rgbMatch[3] ?? \"0\", 10),\n      a: rgbMatch[4] ? Number.parseFloat(rgbMatch[4]) : 1,\n    };\n  }\n\n  // Parse hsl/hsla colors\n  const hslMatch = trimmed.match(\n    /^hsla?\\(\\s*(\\d+)\\s*,\\s*(\\d+)%\\s*,\\s*(\\d+)%\\s*(?:,\\s*([\\d.]+))?\\s*\\)$/,\n  );\n  if (hslMatch) {\n    const h = Number.parseInt(hslMatch[1] ?? \"0\", 10);\n    const s = Number.parseInt(hslMatch[2] ?? \"0\", 10) / 100;\n    const l = Number.parseInt(hslMatch[3] ?? \"0\", 10) / 100;\n    const a = hslMatch[4] ? Number.parseFloat(hslMatch[4]) : 1;\n\n    // Convert HSL to RGB\n    const c = (1 - Math.abs(2 * l - 1)) * s;\n    const x = c * (1 - Math.abs(((h / 60) % 2) - 1));\n    const m = l - c / 2;\n\n    let r = 0;\n    let g = 0;\n    let b = 0;\n\n    if (h >= 0 && h < 60) {\n      r = c;\n      g = x;\n      b = 0;\n    } else if (h >= 60 && h < 120) {\n      r = x;\n      g = c;\n      b = 0;\n    } else if (h >= 120 && h < 180) {\n      r = 0;\n      g = c;\n      b = x;\n    } else if (h >= 180 && h < 240) {\n      r = 0;\n      g = x;\n      b = c;\n    } else if (h >= 240 && h < 300) {\n      r = x;\n      g = 0;\n      b = c;\n    } else if (h >= 300 && h < 360) {\n      r = c;\n      g = 0;\n      b = x;\n    }\n\n    return {\n      r: Math.round((r + m) * 255),\n      g: Math.round((g + m) * 255),\n      b: Math.round((b + m) * 255),\n      a,\n    };\n  }\n\n  // Parse hsb/hsba colors\n  const hsbMatch = trimmed.match(\n    /^hsba?\\(\\s*(\\d+)\\s*,\\s*(\\d+)%\\s*,\\s*(\\d+)%\\s*(?:,\\s*([\\d.]+))?\\s*\\)$/,\n  );\n  if (hsbMatch) {\n    const h = Number.parseInt(hsbMatch[1] ?? \"0\", 10);\n    const s = Number.parseInt(hsbMatch[2] ?? \"0\", 10);\n    const v = Number.parseInt(hsbMatch[3] ?? \"0\", 10);\n    const a = hsbMatch[4] ? Number.parseFloat(hsbMatch[4]) : 1;\n\n    return hsvToRgb({ h, s, v, a });\n  }\n\n  return null;\n}\n\ntype Direction = \"ltr\" | \"rtl\";\n\ninterface StoreState {\n  color: ColorValue;\n  hsv: HSVColorValue;\n  open: boolean;\n  format: ColorFormat;\n}\n\ninterface Store {\n  subscribe: (cb: () => void) => () => void;\n  getState: () => StoreState;\n  setColor: (value: ColorValue) => void;\n  setHsv: (value: HSVColorValue) => void;\n  setOpen: (value: boolean) => void;\n  setFormat: (value: ColorFormat) => void;\n  notify: () => void;\n}\n\nconst StoreContext = React.createContext<Store | null>(null);\n\nfunction useStoreContext(consumerName: string) {\n  const context = React.useContext(StoreContext);\n  if (!context) {\n    throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\n  }\n  return context;\n}\n\nfunction useStore<U>(selector: (state: StoreState) => U): U {\n  const store = useStoreContext(\"useStore\");\n\n  const getSnapshot = React.useCallback(\n    () => selector(store.getState()),\n    [store, selector],\n  );\n\n  return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);\n}\n\ninterface ColorPickerContextValue {\n  dir: Direction;\n  disabled?: boolean;\n  inline?: boolean;\n  readOnly?: boolean;\n  required?: boolean;\n}\n\nconst ColorPickerContext = React.createContext<ColorPickerContextValue | null>(\n  null,\n);\n\nfunction useColorPickerContext(consumerName: string) {\n  const context = React.useContext(ColorPickerContext);\n  if (!context) {\n    throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\n  }\n  return context;\n}\n\ninterface ColorPickerProps\n  extends\n    Omit<DivProps, \"onValueChange\">,\n    Pick<\n      React.ComponentProps<typeof Popover>,\n      \"defaultOpen\" | \"open\" | \"onOpenChange\" | \"modal\"\n    > {\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  dir?: Direction;\n  format?: ColorFormat;\n  defaultFormat?: ColorFormat;\n  onFormatChange?: (format: ColorFormat) => void;\n  name?: string;\n  asChild?: boolean;\n  disabled?: boolean;\n  inline?: boolean;\n  readOnly?: boolean;\n  required?: boolean;\n}\n\nfunction ColorPicker(props: ColorPickerProps) {\n  const {\n    value: valueProp,\n    defaultValue = \"#000000\",\n    onValueChange,\n    format: formatProp,\n    defaultFormat = \"hex\",\n    onFormatChange,\n    defaultOpen,\n    open: openProp,\n    onOpenChange,\n    name,\n    disabled,\n    inline,\n    readOnly,\n    required,\n    ...rootProps\n  } = props;\n\n  const listenersRef = useLazyRef(() => new Set<() => void>());\n  const stateRef = useLazyRef<StoreState>(() => {\n    const colorString = valueProp ?? defaultValue;\n    const color = hexToRgb(colorString);\n\n    return {\n      color,\n      hsv: rgbToHsv(color),\n      open: openProp ?? defaultOpen ?? false,\n      format: formatProp ?? defaultFormat,\n    };\n  });\n\n  const propsRef = useAsRef({\n    onValueChange,\n    onOpenChange,\n    onFormatChange,\n  });\n\n  const store = React.useMemo<Store>(() => {\n    return {\n      subscribe: (cb) => {\n        listenersRef.current.add(cb);\n        return () => listenersRef.current.delete(cb);\n      },\n      getState: () => stateRef.current,\n      setColor: (value: ColorValue) => {\n        if (Object.is(stateRef.current.color, value)) return;\n\n        const prevState = { ...stateRef.current };\n        stateRef.current.color = value;\n\n        if (propsRef.current.onValueChange) {\n          const colorString = colorToString(value, prevState.format);\n          propsRef.current.onValueChange(colorString);\n        }\n\n        store.notify();\n      },\n      setHsv: (value: HSVColorValue) => {\n        if (Object.is(stateRef.current.hsv, value)) return;\n\n        const prevState = { ...stateRef.current };\n        stateRef.current.hsv = value;\n\n        if (propsRef.current.onValueChange) {\n          const colorValue = hsvToRgb(value);\n          const colorString = colorToString(colorValue, prevState.format);\n          propsRef.current.onValueChange(colorString);\n        }\n\n        store.notify();\n      },\n      setOpen: (value: boolean) => {\n        if (Object.is(stateRef.current.open, value)) return;\n\n        stateRef.current.open = value;\n\n        if (propsRef.current.onOpenChange) {\n          propsRef.current.onOpenChange(value);\n        }\n\n        store.notify();\n      },\n      setFormat: (value: ColorFormat) => {\n        if (Object.is(stateRef.current.format, value)) return;\n\n        stateRef.current.format = value;\n\n        if (propsRef.current.onFormatChange) {\n          propsRef.current.onFormatChange(value);\n        }\n\n        store.notify();\n      },\n      notify: () => {\n        for (const cb of listenersRef.current) {\n          cb();\n        }\n      },\n    };\n  }, [listenersRef, stateRef, propsRef]);\n\n  return (\n    <StoreContext.Provider value={store}>\n      <ColorPickerImpl\n        {...rootProps}\n        value={valueProp}\n        defaultOpen={defaultOpen}\n        open={openProp}\n        name={name}\n        disabled={disabled}\n        inline={inline}\n        readOnly={readOnly}\n        required={required}\n      />\n    </StoreContext.Provider>\n  );\n}\n\ninterface ColorPickerImplProps extends Omit<\n  ColorPickerProps,\n  | \"defaultValue\"\n  | \"onValueChange\"\n  | \"onOpenChange\"\n  | \"format\"\n  | \"defaultFormat\"\n  | \"onFormatChange\"\n> {}\n\nfunction ColorPickerImpl(props: ColorPickerImplProps) {\n  const {\n    value: valueProp,\n    dir: dirProp,\n    defaultOpen,\n    open: openProp,\n    name,\n    ref,\n    asChild,\n    disabled,\n    inline,\n    modal,\n    readOnly,\n    required,\n    ...rootProps\n  } = props;\n\n  const store = useStoreContext(ROOT_IMPL_NAME);\n\n  const dir = DirectionPrimitive.useDirection(dirProp);\n\n  const [formTrigger, setFormTrigger] = React.useState<RootElement | null>(\n    null,\n  );\n  const composedRef = useComposedRefs(ref, (node) => setFormTrigger(node));\n  const isFormControl = formTrigger ? !!formTrigger.closest(\"form\") : true;\n\n  useIsomorphicLayoutEffect(() => {\n    if (valueProp !== undefined) {\n      const currentState = store.getState();\n      const color = hexToRgb(valueProp, currentState.color.a);\n      const hsv = rgbToHsv(color);\n      store.setColor(color);\n      store.setHsv(hsv);\n    }\n  }, [valueProp]);\n\n  useIsomorphicLayoutEffect(() => {\n    if (openProp !== undefined) {\n      store.setOpen(openProp);\n    }\n  }, [openProp]);\n\n  const contextValue = React.useMemo<ColorPickerContextValue>(\n    () => ({\n      dir,\n      disabled,\n      inline,\n      readOnly,\n      required,\n    }),\n    [dir, disabled, inline, readOnly, required],\n  );\n\n  const value = useStore((state) => rgbToHex(state.color));\n  const open = useStore((state) => state.open);\n\n  const RootPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  if (inline) {\n    return (\n      <ColorPickerContext.Provider value={contextValue}>\n        <RootPrimitive {...rootProps} ref={composedRef} />\n        {isFormControl && (\n          <VisuallyHiddenInput\n            type=\"hidden\"\n            control={formTrigger}\n            name={name}\n            value={value}\n            disabled={disabled}\n            readOnly={readOnly}\n            required={required}\n          />\n        )}\n      </ColorPickerContext.Provider>\n    );\n  }\n\n  return (\n    <ColorPickerContext.Provider value={contextValue}>\n      <Popover\n        defaultOpen={defaultOpen}\n        open={open}\n        onOpenChange={store.setOpen}\n        modal={modal}\n      >\n        <RootPrimitive {...rootProps} ref={composedRef} />\n        {isFormControl && (\n          <VisuallyHiddenInput\n            type=\"hidden\"\n            control={formTrigger}\n            name={name}\n            value={value}\n            disabled={disabled}\n            readOnly={readOnly}\n            required={required}\n          />\n        )}\n      </Popover>\n    </ColorPickerContext.Provider>\n  );\n}\n\nfunction ColorPickerTrigger(\n  props: React.ComponentProps<typeof PopoverTrigger>,\n) {\n  const { asChild, disabled, ...triggerProps } = props;\n\n  const context = useColorPickerContext(TRIGGER_NAME);\n\n  const isDisabled = disabled || context.disabled;\n\n  const TriggerPrimitive = asChild ? SlotPrimitive.Slot : Button;\n\n  return (\n    <PopoverTrigger asChild disabled={isDisabled}>\n      <TriggerPrimitive data-slot=\"color-picker-trigger\" {...triggerProps} />\n    </PopoverTrigger>\n  );\n}\n\nfunction ColorPickerContent(\n  props: React.ComponentProps<typeof PopoverContent>,\n) {\n  const { asChild, className, children, ...popoverContentProps } = props;\n\n  const context = useColorPickerContext(CONTENT_NAME);\n\n  if (context.inline) {\n    const ContentPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n    return (\n      <ContentPrimitive\n        data-slot=\"color-picker-content\"\n        {...popoverContentProps}\n        className={cn(\"flex w-[340px] flex-col gap-4 p-4\", className)}\n      >\n        {children}\n      </ContentPrimitive>\n    );\n  }\n\n  return (\n    <PopoverContent\n      data-slot=\"color-picker-content\"\n      asChild={asChild}\n      {...popoverContentProps}\n      className={cn(\"flex w-[340px] flex-col gap-4 p-4\", className)}\n    >\n      {children}\n    </PopoverContent>\n  );\n}\n\nfunction ColorPickerArea(props: DivProps) {\n  const {\n    asChild,\n    onPointerDown: onPointerDownProp,\n    onPointerMove: onPointerMoveProp,\n    onPointerUp: onPointerUpProp,\n    className,\n    ref,\n    ...areaProps\n  } = props;\n\n  const propsRef = useAsRef({\n    onPointerDown: onPointerDownProp,\n    onPointerMove: onPointerMoveProp,\n    onPointerUp: onPointerUpProp,\n  });\n\n  const context = useColorPickerContext(AREA_NAME);\n  const store = useStoreContext(AREA_NAME);\n\n  const hsv = useStore((state) => state.hsv);\n\n  const isDraggingRef = React.useRef(false);\n  const areaRef = React.useRef<HTMLDivElement>(null);\n  const composedRef = useComposedRefs(ref, areaRef);\n\n  const updateColorFromPosition = React.useCallback(\n    (clientX: number, clientY: number) => {\n      if (!areaRef.current) return;\n\n      const rect = areaRef.current.getBoundingClientRect();\n      const x = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));\n      const y = Math.max(\n        0,\n        Math.min(1, 1 - (clientY - rect.top) / rect.height),\n      );\n\n      const newHsv: HSVColorValue = {\n        h: hsv?.h ?? 0,\n        s: Math.round(x * 100),\n        v: Math.round(y * 100),\n        a: hsv?.a ?? 1,\n      };\n\n      store.setHsv(newHsv);\n      store.setColor(hsvToRgb(newHsv));\n    },\n    [hsv, store],\n  );\n\n  const onPointerDown = React.useCallback(\n    (event: React.PointerEvent<AreaElement>) => {\n      if (context.disabled) return;\n      propsRef.current.onPointerDown?.(event);\n      if (event.defaultPrevented) return;\n\n      isDraggingRef.current = true;\n      areaRef.current?.setPointerCapture(event.pointerId);\n      updateColorFromPosition(event.clientX, event.clientY);\n    },\n    [context.disabled, updateColorFromPosition, propsRef],\n  );\n\n  const onPointerMove = React.useCallback(\n    (event: React.PointerEvent<AreaElement>) => {\n      propsRef.current.onPointerMove?.(event);\n      if (event.defaultPrevented) return;\n\n      if (isDraggingRef.current) {\n        updateColorFromPosition(event.clientX, event.clientY);\n      }\n    },\n    [updateColorFromPosition, propsRef],\n  );\n\n  const onPointerUp = React.useCallback(\n    (event: React.PointerEvent<AreaElement>) => {\n      propsRef.current.onPointerUp?.(event);\n      if (event.defaultPrevented) return;\n\n      isDraggingRef.current = false;\n      areaRef.current?.releasePointerCapture(event.pointerId);\n    },\n    [propsRef],\n  );\n\n  const hue = hsv?.h ?? 0;\n  const backgroundHue = hsvToRgb({ h: hue, s: 100, v: 100, a: 1 });\n\n  const AreaPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <AreaPrimitive\n      data-slot=\"color-picker-area\"\n      {...areaProps}\n      className={cn(\n        \"relative h-40 w-full cursor-crosshair touch-none rounded-sm border\",\n        context.disabled && \"pointer-events-none opacity-50\",\n        className,\n      )}\n      ref={composedRef}\n      onPointerDown={onPointerDown}\n      onPointerMove={onPointerMove}\n      onPointerUp={onPointerUp}\n    >\n      <div className=\"absolute inset-0 overflow-hidden rounded-sm\">\n        <div\n          className=\"absolute inset-0\"\n          style={{\n            backgroundColor: `rgb(${backgroundHue.r}, ${backgroundHue.g}, ${backgroundHue.b})`,\n          }}\n        />\n        <div\n          className=\"absolute inset-0\"\n          style={{\n            background: \"linear-gradient(to right, #fff, transparent)\",\n          }}\n        />\n        <div\n          className=\"absolute inset-0\"\n          style={{\n            background: \"linear-gradient(to bottom, transparent, #000)\",\n          }}\n        />\n      </div>\n      <div\n        className=\"absolute size-3 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white shadow-sm\"\n        style={{\n          left: `${hsv?.s ?? 0}%`,\n          top: `${100 - (hsv?.v ?? 0)}%`,\n        }}\n      />\n    </AreaPrimitive>\n  );\n}\n\nfunction ColorPickerHueSlider(\n  props: React.ComponentProps<typeof SliderPrimitive.Root>,\n) {\n  const { className, ...sliderProps } = props;\n\n  const context = useColorPickerContext(HUE_SLIDER_NAME);\n  const store = useStoreContext(HUE_SLIDER_NAME);\n\n  const hsv = useStore((state) => state.hsv);\n\n  const onValueChange = React.useCallback(\n    (values: number[]) => {\n      const newHsv: HSVColorValue = {\n        h: values[0] ?? 0,\n        s: hsv?.s ?? 0,\n        v: hsv?.v ?? 0,\n        a: hsv?.a ?? 1,\n      };\n      store.setHsv(newHsv);\n      store.setColor(hsvToRgb(newHsv));\n    },\n    [hsv, store],\n  );\n\n  return (\n    <SliderPrimitive.Root\n      data-slot=\"color-picker-hue-slider\"\n      {...sliderProps}\n      max={360}\n      step={1}\n      className={cn(\n        \"relative flex w-full touch-none items-center select-none\",\n        className,\n      )}\n      value={[hsv?.h ?? 0]}\n      onValueChange={onValueChange}\n      disabled={context.disabled}\n    >\n      <SliderPrimitive.Track className=\"relative h-3 w-full grow overflow-hidden rounded-full bg-[linear-gradient(to_right,#ff0000_0%,#ffff00_16.66%,#00ff00_33.33%,#00ffff_50%,#0000ff_66.66%,#ff00ff_83.33%,#ff0000_100%)]\">\n        <SliderPrimitive.Range className=\"absolute h-full\" />\n      </SliderPrimitive.Track>\n      <SliderPrimitive.Thumb className=\"block size-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50\" />\n    </SliderPrimitive.Root>\n  );\n}\n\nfunction ColorPickerAlphaSlider(\n  props: React.ComponentProps<typeof SliderPrimitive.Root>,\n) {\n  const { className, ...sliderProps } = props;\n\n  const context = useColorPickerContext(ALPHA_SLIDER_NAME);\n  const store = useStoreContext(ALPHA_SLIDER_NAME);\n\n  const color = useStore((state) => state.color);\n  const hsv = useStore((state) => state.hsv);\n\n  const onValueChange = React.useCallback(\n    (values: number[]) => {\n      const alpha = (values[0] ?? 0) / 100;\n      const newColor = { ...color, a: alpha };\n      const newHsv = { ...hsv, a: alpha };\n      store.setColor(newColor);\n      store.setHsv(newHsv);\n    },\n    [color, hsv, store],\n  );\n\n  const gradientColor = `rgb(${color?.r ?? 0}, ${color?.g ?? 0}, ${color?.b ?? 0})`;\n\n  return (\n    <SliderPrimitive.Root\n      data-slot=\"color-picker-alpha-slider\"\n      {...sliderProps}\n      max={100}\n      step={1}\n      disabled={context.disabled}\n      className={cn(\n        \"relative flex w-full touch-none items-center select-none\",\n        className,\n      )}\n      value={[Math.round((color?.a ?? 1) * 100)]}\n      onValueChange={onValueChange}\n    >\n      <SliderPrimitive.Track\n        className=\"relative h-3 w-full grow overflow-hidden rounded-full\"\n        style={{\n          background:\n            \"linear-gradient(45deg, #ccc 25%, transparent 25%), linear-gradient(-45deg, #ccc 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #ccc 75%), linear-gradient(-45deg, transparent 75%, #ccc 75%)\",\n          backgroundSize: \"8px 8px\",\n          backgroundPosition: \"0 0, 0 4px, 4px -4px, -4px 0px\",\n        }}\n      >\n        <div\n          className=\"absolute inset-0 rounded-full\"\n          style={{\n            background: `linear-gradient(to right, transparent, ${gradientColor})`,\n          }}\n        />\n        <SliderPrimitive.Range className=\"absolute h-full\" />\n      </SliderPrimitive.Track>\n      <SliderPrimitive.Thumb className=\"block size-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50\" />\n    </SliderPrimitive.Root>\n  );\n}\n\nfunction ColorPickerSwatch(props: DivProps) {\n  const { asChild, className, ...swatchProps } = props;\n\n  const context = useColorPickerContext(SWATCH_NAME);\n\n  const color = useStore((state) => state.color);\n  const format = useStore((state) => state.format);\n\n  const backgroundStyle = React.useMemo(() => {\n    if (!color) {\n      return {\n        background:\n          \"linear-gradient(to bottom right, transparent calc(50% - 1px), hsl(var(--destructive)) calc(50% - 1px) calc(50% + 1px), transparent calc(50% + 1px)) no-repeat\",\n      };\n    }\n\n    const colorString = `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})`;\n\n    if (color.a < 1) {\n      return {\n        background: `linear-gradient(${colorString}, ${colorString}), repeating-conic-gradient(#ccc 0% 25%, #fff 0% 50%) 0% 50% / 8px 8px`,\n      };\n    }\n\n    return {\n      backgroundColor: colorString,\n    };\n  }, [color]);\n\n  const ariaLabel = !color\n    ? \"No color selected\"\n    : `Current color: ${colorToString(color, format)}`;\n\n  const SwatchPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <SwatchPrimitive\n      role=\"img\"\n      aria-label={ariaLabel}\n      data-slot=\"color-picker-swatch\"\n      {...swatchProps}\n      className={cn(\n        \"box-border size-8 rounded-sm border shadow-sm\",\n        context.disabled && \"opacity-50\",\n        className,\n      )}\n      style={{\n        ...backgroundStyle,\n        forcedColorAdjust: \"none\",\n      }}\n    />\n  );\n}\n\nfunction ColorPickerEyeDropper(props: React.ComponentProps<typeof Button>) {\n  const { size: sizeProp, children, disabled, ...buttonProps } = props;\n\n  const context = useColorPickerContext(EYE_DROPPER_NAME);\n  const store = useStoreContext(EYE_DROPPER_NAME);\n\n  const color = useStore((state) => state.color);\n\n  const isDisabled = disabled || context.disabled;\n\n  const onEyeDropper = React.useCallback(async () => {\n    if (!window.EyeDropper) return;\n\n    try {\n      const eyeDropper = new window.EyeDropper();\n      const result = await eyeDropper.open();\n\n      if (result.sRGBHex) {\n        const currentAlpha = color?.a ?? 1;\n        const newColor = hexToRgb(result.sRGBHex, currentAlpha);\n        const newHsv = rgbToHsv(newColor);\n        store.setColor(newColor);\n        store.setHsv(newHsv);\n      }\n    } catch (error) {\n      console.warn(\"EyeDropper error:\", error);\n    }\n  }, [color, store]);\n\n  const hasEyeDropper = typeof window !== \"undefined\" && !!window.EyeDropper;\n\n  if (!hasEyeDropper) return null;\n\n  const size = sizeProp ?? (children ? \"default\" : \"icon\");\n\n  return (\n    <Button\n      data-slot=\"color-picker-eye-dropper\"\n      {...buttonProps}\n      variant=\"outline\"\n      size={size}\n      onClick={onEyeDropper}\n      disabled={isDisabled}\n    >\n      {children ?? (\n        <IconPlaceholder\n          lucide=\"PipetteIcon\"\n          tabler=\"IconColorPicker\"\n          hugeicons=\"ColorPickerIcon\"\n          phosphor=\"EyedropperIcon\"\n          remixicon=\"RiDropperLine\"\n        />\n      )}\n    </Button>\n  );\n}\n\ninterface ColorPickerFormatSelectProps\n  extends\n    Omit<React.ComponentProps<typeof Select>, \"value\" | \"onValueChange\">,\n    Pick<React.ComponentProps<typeof SelectTrigger>, \"size\" | \"className\"> {}\n\nfunction ColorPickerFormatSelect(props: ColorPickerFormatSelectProps) {\n  const { size, disabled, className, ...selectProps } = props;\n\n  const context = useColorPickerContext(FORMAT_SELECT_NAME);\n  const store = useStoreContext(FORMAT_SELECT_NAME);\n  const isDisabled = disabled || context.disabled;\n\n  const format = useStore((state) => state.format);\n\n  const onFormatChange = React.useCallback(\n    (value: ColorFormat) => {\n      store.setFormat(value);\n    },\n    [store],\n  );\n\n  return (\n    <Select\n      data-slot=\"color-picker-format-select\"\n      {...selectProps}\n      value={format}\n      onValueChange={onFormatChange}\n      disabled={isDisabled}\n    >\n      <SelectTrigger\n        data-slot=\"color-picker-format-select-trigger\"\n        size={size ?? \"sm\"}\n        className={cn(className)}\n      >\n        <SelectValue />\n      </SelectTrigger>\n      <SelectContent>\n        {colorFormats.map((format) => (\n          <SelectItem key={format} value={format}>\n            {format.toUpperCase()}\n          </SelectItem>\n        ))}\n      </SelectContent>\n    </Select>\n  );\n}\n\ninterface ColorPickerInputProps extends Omit<\n  React.ComponentProps<typeof Input>,\n  \"value\" | \"onChange\" | \"color\"\n> {\n  withoutAlpha?: boolean;\n}\n\nfunction ColorPickerInput(props: ColorPickerInputProps) {\n  const store = useStoreContext(INPUT_NAME);\n  const context = useColorPickerContext(INPUT_NAME);\n\n  const color = useStore((state) => state.color);\n  const format = useStore((state) => state.format);\n  const hsv = useStore((state) => state.hsv);\n\n  const onColorChange = React.useCallback(\n    (newColor: ColorValue) => {\n      const newHsv = rgbToHsv(newColor);\n      store.setColor(newColor);\n      store.setHsv(newHsv);\n    },\n    [store],\n  );\n\n  if (format === \"hex\") {\n    return (\n      <HexInput\n        color={color}\n        onColorChange={onColorChange}\n        context={context}\n        {...props}\n      />\n    );\n  }\n\n  if (format === \"rgb\") {\n    return (\n      <RgbInput\n        color={color}\n        onColorChange={onColorChange}\n        context={context}\n        {...props}\n      />\n    );\n  }\n\n  if (format === \"hsl\") {\n    return (\n      <HslInput\n        color={color}\n        onColorChange={onColorChange}\n        context={context}\n        {...props}\n      />\n    );\n  }\n\n  if (format === \"hsb\") {\n    return (\n      <HsbInput\n        hsv={hsv}\n        onColorChange={onColorChange}\n        context={context}\n        {...props}\n      />\n    );\n  }\n}\n\nconst inputGroupItemVariants = cva(\n  \"h-8 [-moz-appearance:textfield] focus-visible:z-10 focus-visible:ring-1 [&::-webkit-inner-spin-button]:m-0 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:m-0 [&::-webkit-outer-spin-button]:appearance-none\",\n  {\n    variants: {\n      position: {\n        first: \"rounded-e-none\",\n        middle: \"-ms-px rounded-none border-l-0\",\n        last: \"-ms-px rounded-s-none border-l-0\",\n        isolated: \"\",\n      },\n    },\n    defaultVariants: {\n      position: \"isolated\",\n    },\n  },\n);\n\ninterface InputGroupItemProps\n  extends\n    React.ComponentProps<typeof Input>,\n    VariantProps<typeof inputGroupItemVariants> {}\n\nfunction InputGroupItem({\n  className,\n  position,\n  ...props\n}: InputGroupItemProps) {\n  return (\n    <Input\n      data-slot=\"color-picker-input\"\n      className={cn(inputGroupItemVariants({ position, className }))}\n      {...props}\n    />\n  );\n}\n\ninterface FormatInputProps extends ColorPickerInputProps {\n  color: ColorValue;\n  onColorChange: (color: ColorValue) => void;\n  context: ColorPickerContextValue;\n}\n\nfunction HexInput(props: FormatInputProps) {\n  const {\n    color,\n    onColorChange,\n    context,\n    withoutAlpha,\n    className,\n    ...inputProps\n  } = props;\n\n  const hexValue = rgbToHex(color);\n  const alphaValue = Math.round((color?.a ?? 1) * 100);\n\n  const onHexChange = React.useCallback(\n    (event: React.ChangeEvent<InputElement>) => {\n      const value = event.target.value;\n      const parsedColor = parseColorString(value);\n      if (parsedColor) {\n        onColorChange({ ...parsedColor, a: color?.a ?? 1 });\n      }\n    },\n    [color, onColorChange],\n  );\n\n  const onAlphaChange = React.useCallback(\n    (event: React.ChangeEvent<InputElement>) => {\n      const value = Number.parseInt(event.target.value, 10);\n      if (!Number.isNaN(value) && value >= 0 && value <= 100) {\n        onColorChange({ ...color, a: value / 100 });\n      }\n    },\n    [color, onColorChange],\n  );\n\n  if (withoutAlpha) {\n    return (\n      <InputGroupItem\n        aria-label=\"Hex color value\"\n        position=\"isolated\"\n        {...inputProps}\n        placeholder=\"#000000\"\n        className={cn(\"font-mono\", className)}\n        value={hexValue}\n        onChange={onHexChange}\n        disabled={context.disabled}\n      />\n    );\n  }\n\n  return (\n    <div\n      data-slot=\"color-picker-input-wrapper\"\n      className={cn(\"flex items-center\", className)}\n    >\n      <InputGroupItem\n        aria-label=\"Hex color value\"\n        position=\"first\"\n        {...inputProps}\n        placeholder=\"#000000\"\n        className=\"flex-1 font-mono\"\n        value={hexValue}\n        onChange={onHexChange}\n        disabled={context.disabled}\n      />\n      <InputGroupItem\n        aria-label=\"Alpha transparency percentage\"\n        position=\"last\"\n        {...inputProps}\n        placeholder=\"100\"\n        inputMode=\"numeric\"\n        pattern=\"[0-9]*\"\n        min=\"0\"\n        max=\"100\"\n        className=\"w-14\"\n        value={alphaValue}\n        onChange={onAlphaChange}\n        disabled={context.disabled}\n      />\n    </div>\n  );\n}\n\nfunction RgbInput(props: FormatInputProps) {\n  const {\n    color,\n    onColorChange,\n    context,\n    withoutAlpha,\n    className,\n    ...inputProps\n  } = props;\n\n  const rValue = Math.round(color?.r ?? 0);\n  const gValue = Math.round(color?.g ?? 0);\n  const bValue = Math.round(color?.b ?? 0);\n  const alphaValue = Math.round((color?.a ?? 1) * 100);\n\n  const onChannelChange = React.useCallback(\n    (channel: \"r\" | \"g\" | \"b\" | \"a\", max: number, isAlpha = false) =>\n      (event: React.ChangeEvent<InputElement>) => {\n        const value = Number.parseInt(event.target.value, 10);\n        if (!Number.isNaN(value) && value >= 0 && value <= max) {\n          const newValue = isAlpha ? value / 100 : value;\n          onColorChange({ ...color, [channel]: newValue });\n        }\n      },\n    [color, onColorChange],\n  );\n\n  return (\n    <div\n      data-slot=\"color-picker-input-wrapper\"\n      className={cn(\"flex items-center\", className)}\n    >\n      <InputGroupItem\n        aria-label=\"Red color component (0-255)\"\n        position=\"first\"\n        {...inputProps}\n        placeholder=\"0\"\n        inputMode=\"numeric\"\n        pattern=\"[0-9]*\"\n        min=\"0\"\n        max=\"255\"\n        className=\"w-14\"\n        value={rValue}\n        onChange={onChannelChange(\"r\", 255)}\n        disabled={context.disabled}\n      />\n      <InputGroupItem\n        aria-label=\"Green color component (0-255)\"\n        position=\"middle\"\n        {...inputProps}\n        placeholder=\"0\"\n        inputMode=\"numeric\"\n        pattern=\"[0-9]*\"\n        min=\"0\"\n        max=\"255\"\n        className=\"w-14\"\n        value={gValue}\n        onChange={onChannelChange(\"g\", 255)}\n        disabled={context.disabled}\n      />\n      <InputGroupItem\n        aria-label=\"Blue color component (0-255)\"\n        position={withoutAlpha ? \"last\" : \"middle\"}\n        {...inputProps}\n        placeholder=\"0\"\n        inputMode=\"numeric\"\n        pattern=\"[0-9]*\"\n        min=\"0\"\n        max=\"255\"\n        className=\"w-14\"\n        value={bValue}\n        onChange={onChannelChange(\"b\", 255)}\n        disabled={context.disabled}\n      />\n      {!withoutAlpha && (\n        <InputGroupItem\n          aria-label=\"Alpha transparency percentage\"\n          position=\"last\"\n          {...inputProps}\n          placeholder=\"100\"\n          inputMode=\"numeric\"\n          pattern=\"[0-9]*\"\n          min=\"0\"\n          max=\"100\"\n          className=\"w-14\"\n          value={alphaValue}\n          onChange={onChannelChange(\"a\", 100, true)}\n          disabled={context.disabled}\n        />\n      )}\n    </div>\n  );\n}\n\nfunction HslInput(props: FormatInputProps) {\n  const {\n    color,\n    onColorChange,\n    context,\n    withoutAlpha,\n    className,\n    ...inputProps\n  } = props;\n\n  const hsl = React.useMemo(() => rgbToHsl(color), [color]);\n  const alphaValue = Math.round((color?.a ?? 1) * 100);\n\n  const onHslChannelChange = React.useCallback(\n    (channel: \"h\" | \"s\" | \"l\", max: number) =>\n      (event: React.ChangeEvent<InputElement>) => {\n        const value = Number.parseInt(event.target.value, 10);\n        if (!Number.isNaN(value) && value >= 0 && value <= max) {\n          const newHsl = { ...hsl, [channel]: value };\n          const newColor = hslToRgb(newHsl, color?.a ?? 1);\n          onColorChange(newColor);\n        }\n      },\n    [hsl, color, onColorChange],\n  );\n\n  const onAlphaChange = React.useCallback(\n    (event: React.ChangeEvent<InputElement>) => {\n      const value = Number.parseInt(event.target.value, 10);\n      if (!Number.isNaN(value) && value >= 0 && value <= 100) {\n        onColorChange({ ...color, a: value / 100 });\n      }\n    },\n    [color, onColorChange],\n  );\n\n  return (\n    <div\n      data-slot=\"color-picker-input-wrapper\"\n      className={cn(\"flex items-center\", className)}\n    >\n      <InputGroupItem\n        aria-label=\"Hue degree (0-360)\"\n        position=\"first\"\n        {...inputProps}\n        placeholder=\"0\"\n        inputMode=\"numeric\"\n        pattern=\"[0-9]*\"\n        min=\"0\"\n        max=\"360\"\n        className=\"w-14\"\n        value={hsl.h}\n        onChange={onHslChannelChange(\"h\", 360)}\n        disabled={context.disabled}\n      />\n      <InputGroupItem\n        aria-label=\"Saturation percentage (0-100)\"\n        position=\"middle\"\n        {...inputProps}\n        placeholder=\"0\"\n        inputMode=\"numeric\"\n        pattern=\"[0-9]*\"\n        min=\"0\"\n        max=\"100\"\n        className=\"w-14\"\n        value={hsl.s}\n        onChange={onHslChannelChange(\"s\", 100)}\n        disabled={context.disabled}\n      />\n      <InputGroupItem\n        aria-label=\"Lightness percentage (0-100)\"\n        position={withoutAlpha ? \"last\" : \"middle\"}\n        {...inputProps}\n        placeholder=\"0\"\n        inputMode=\"numeric\"\n        pattern=\"[0-9]*\"\n        min=\"0\"\n        max=\"100\"\n        className=\"w-14\"\n        value={hsl.l}\n        onChange={onHslChannelChange(\"l\", 100)}\n        disabled={context.disabled}\n      />\n      {!withoutAlpha && (\n        <InputGroupItem\n          aria-label=\"Alpha transparency percentage\"\n          position=\"last\"\n          {...inputProps}\n          placeholder=\"100\"\n          inputMode=\"numeric\"\n          pattern=\"[0-9]*\"\n          min=\"0\"\n          max=\"100\"\n          className=\"w-14\"\n          value={alphaValue}\n          onChange={onAlphaChange}\n          disabled={context.disabled}\n        />\n      )}\n    </div>\n  );\n}\n\ninterface HsbInputProps extends Omit<FormatInputProps, \"color\"> {\n  hsv: HSVColorValue;\n}\n\nfunction HsbInput(props: HsbInputProps) {\n  const {\n    hsv,\n    onColorChange,\n    context,\n    withoutAlpha,\n    className,\n    ...inputProps\n  } = props;\n\n  const alphaValue = Math.round((hsv?.a ?? 1) * 100);\n\n  const onHsvChannelChange = React.useCallback(\n    (channel: \"h\" | \"s\" | \"v\", max: number) =>\n      (event: React.ChangeEvent<InputElement>) => {\n        const value = Number.parseInt(event.target.value, 10);\n        if (!Number.isNaN(value) && value >= 0 && value <= max) {\n          const newHsv = { ...hsv, [channel]: value };\n          const newColor = hsvToRgb(newHsv);\n          onColorChange(newColor);\n        }\n      },\n    [hsv, onColorChange],\n  );\n\n  const onAlphaChange = React.useCallback(\n    (event: React.ChangeEvent<InputElement>) => {\n      const value = Number.parseInt(event.target.value, 10);\n      if (!Number.isNaN(value) && value >= 0 && value <= 100) {\n        const currentColor = hsvToRgb(hsv);\n        onColorChange({ ...currentColor, a: value / 100 });\n      }\n    },\n    [hsv, onColorChange],\n  );\n\n  return (\n    <div\n      data-slot=\"color-picker-input-wrapper\"\n      className={cn(\"flex items-center\", className)}\n    >\n      <InputGroupItem\n        aria-label=\"Hue degree (0-360)\"\n        position=\"first\"\n        {...inputProps}\n        placeholder=\"0\"\n        inputMode=\"numeric\"\n        pattern=\"[0-9]*\"\n        min=\"0\"\n        max=\"360\"\n        className=\"w-14\"\n        value={hsv?.h ?? 0}\n        onChange={onHsvChannelChange(\"h\", 360)}\n        disabled={context.disabled}\n      />\n      <InputGroupItem\n        aria-label=\"Saturation percentage (0-100)\"\n        position=\"middle\"\n        {...inputProps}\n        placeholder=\"0\"\n        inputMode=\"numeric\"\n        pattern=\"[0-9]*\"\n        min=\"0\"\n        max=\"100\"\n        className=\"w-14\"\n        value={hsv?.s ?? 0}\n        onChange={onHsvChannelChange(\"s\", 100)}\n        disabled={context.disabled}\n      />\n      <InputGroupItem\n        aria-label=\"Brightness percentage (0-100)\"\n        position={withoutAlpha ? \"last\" : \"middle\"}\n        {...inputProps}\n        placeholder=\"0\"\n        inputMode=\"numeric\"\n        pattern=\"[0-9]*\"\n        min=\"0\"\n        max=\"100\"\n        className=\"w-14\"\n        value={hsv?.v ?? 0}\n        onChange={onHsvChannelChange(\"v\", 100)}\n        disabled={context.disabled}\n      />\n      {!withoutAlpha && (\n        <InputGroupItem\n          aria-label=\"Alpha transparency percentage\"\n          position=\"last\"\n          {...inputProps}\n          placeholder=\"100\"\n          inputMode=\"numeric\"\n          pattern=\"[0-9]*\"\n          min=\"0\"\n          max=\"100\"\n          className=\"w-14\"\n          value={alphaValue}\n          onChange={onAlphaChange}\n          disabled={context.disabled}\n        />\n      )}\n    </div>\n  );\n}\n\nexport {\n  ColorPicker,\n  ColorPickerAlphaSlider,\n  ColorPickerArea,\n  ColorPickerContent,\n  ColorPickerEyeDropper,\n  ColorPickerFormatSelect,\n  ColorPickerHueSlider,\n  ColorPickerInput,\n  type ColorPickerProps,\n  ColorPickerSwatch,\n  ColorPickerTrigger,\n  useStore as useColorPicker,\n};\n",
      "target": ""
    },
    {
      "path": "components/visually-hidden-input.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\ntype InputValue = string[] | string;\n\ninterface VisuallyHiddenInputProps<T = InputValue> extends Omit<\n  React.InputHTMLAttributes<HTMLInputElement>,\n  \"value\" | \"checked\" | \"onReset\"\n> {\n  value?: T;\n  checked?: boolean;\n  control: HTMLElement | null;\n  bubbles?: boolean;\n}\n\nfunction VisuallyHiddenInput<T = InputValue>(\n  props: VisuallyHiddenInputProps<T>,\n) {\n  const {\n    control,\n    value,\n    checked,\n    bubbles = true,\n    type = \"hidden\",\n    style,\n    ...inputProps\n  } = props;\n\n  const isCheckInput = React.useMemo(\n    () => type === \"checkbox\" || type === \"radio\" || type === \"switch\",\n    [type],\n  );\n  const inputRef = React.useRef<HTMLInputElement>(null);\n\n  const prevValueRef = React.useRef<{\n    value: T | boolean | undefined;\n    previous: T | boolean | undefined;\n  }>({\n    value: isCheckInput ? checked : value,\n    previous: isCheckInput ? checked : value,\n  });\n\n  const prevValue = React.useMemo(() => {\n    const currentValue = isCheckInput ? checked : value;\n    if (prevValueRef.current.value !== currentValue) {\n      prevValueRef.current.previous = prevValueRef.current.value;\n      prevValueRef.current.value = currentValue;\n    }\n    return prevValueRef.current.previous;\n  }, [isCheckInput, value, checked]);\n\n  const [controlSize, setControlSize] = React.useState<{\n    width?: number;\n    height?: number;\n  }>({});\n\n  React.useLayoutEffect(() => {\n    if (!control) {\n      setControlSize({});\n      return;\n    }\n\n    setControlSize({\n      width: control.offsetWidth,\n      height: control.offsetHeight,\n    });\n\n    if (typeof window === \"undefined\") return;\n\n    const resizeObserver = new ResizeObserver((entries) => {\n      if (!Array.isArray(entries) || !entries.length) return;\n\n      const entry = entries[0];\n      if (!entry) return;\n\n      let width: number;\n      let height: number;\n\n      if (\"borderBoxSize\" in entry) {\n        const borderSizeEntry = entry.borderBoxSize;\n        const borderSize = Array.isArray(borderSizeEntry)\n          ? borderSizeEntry[0]\n          : borderSizeEntry;\n        width = borderSize.inlineSize;\n        height = borderSize.blockSize;\n      } else {\n        width = control.offsetWidth;\n        height = control.offsetHeight;\n      }\n\n      setControlSize({ width, height });\n    });\n\n    resizeObserver.observe(control, { box: \"border-box\" });\n    return () => {\n      resizeObserver.disconnect();\n    };\n  }, [control]);\n\n  React.useEffect(() => {\n    const input = inputRef.current;\n    if (!input) return;\n\n    const inputProto = window.HTMLInputElement.prototype;\n    const propertyKey = isCheckInput ? \"checked\" : \"value\";\n    const eventType = isCheckInput ? \"click\" : \"input\";\n    const currentValue = isCheckInput ? checked : value;\n\n    const serializedCurrentValue = isCheckInput\n      ? checked\n      : typeof value === \"object\" && value !== null\n        ? JSON.stringify(value)\n        : value;\n\n    const descriptor = Object.getOwnPropertyDescriptor(inputProto, propertyKey);\n\n    const setter = descriptor?.set;\n\n    if (prevValue !== currentValue && setter) {\n      const event = new Event(eventType, { bubbles });\n      setter.call(input, serializedCurrentValue);\n      input.dispatchEvent(event);\n    }\n  }, [prevValue, value, checked, bubbles, isCheckInput]);\n\n  const composedStyle = React.useMemo<React.CSSProperties>(() => {\n    return {\n      ...style,\n      ...(controlSize.width !== undefined && controlSize.height !== undefined\n        ? controlSize\n        : {}),\n      border: 0,\n      clip: \"rect(0 0 0 0)\",\n      clipPath: \"inset(50%)\",\n      height: \"1px\",\n      margin: \"-1px\",\n      overflow: \"hidden\",\n      padding: 0,\n      position: \"absolute\",\n      whiteSpace: \"nowrap\",\n      width: \"1px\",\n    };\n  }, [style, controlSize]);\n\n  return (\n    <input\n      type={type}\n      {...inputProps}\n      ref={inputRef}\n      aria-hidden={isCheckInput}\n      tabIndex={-1}\n      defaultChecked={isCheckInput ? checked : undefined}\n      style={composedStyle}\n    />\n  );\n}\n\nexport { VisuallyHiddenInput };\n",
      "target": ""
    },
    {
      "path": "lib/compose-refs.ts",
      "type": "registry:lib",
      "content": "/**\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/compose-refs.tsx\n */\n\nimport * as React from \"react\";\n\ntype PossibleRef<T> = React.Ref<T> | undefined;\n\n/**\n * Set a given ref to a given value\n * This utility takes care of different types of refs: callback refs and RefObject(s)\n */\nfunction setRef<T>(ref: PossibleRef<T>, value: T) {\n  if (typeof ref === \"function\") {\n    return ref(value);\n  }\n\n  if (ref !== null && ref !== undefined) {\n    ref.current = value;\n  }\n}\n\n/**\n * A utility to compose multiple refs together\n * Accepts callback refs and RefObject(s)\n */\nfunction composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  return (node) => {\n    let hasCleanup = false;\n    const cleanups = refs.map((ref) => {\n      const cleanup = setRef(ref, node);\n      if (!hasCleanup && typeof cleanup === \"function\") {\n        hasCleanup = true;\n      }\n      return cleanup;\n    });\n\n    // React <19 will log an error to the console if a callback ref returns a\n    // value. We don't use ref cleanups internally so this will only happen if a\n    // user's ref callback returns a value, which we only expect if they are\n    // using the cleanup functionality added in React 19.\n    if (hasCleanup) {\n      return () => {\n        for (let i = 0; i < cleanups.length; i++) {\n          const cleanup = cleanups[i];\n          if (typeof cleanup === \"function\") {\n            cleanup();\n          } else {\n            setRef(refs[i], null);\n          }\n        }\n      };\n    }\n  };\n}\n\n/**\n * A custom hook that composes multiple refs\n * Accepts callback refs and RefObject(s)\n */\nfunction useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  // oxlint-disable-next-line react/exhaustive-deps -- we want to memoize by all values\n  return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { composeRefs, useComposedRefs };\n",
      "target": ""
    }
  ]
}