{ "name": "color-picker", "type": "registry:ui", "dependencies": [ "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 { PipetteIcon } from \"lucide-react\";\nimport {\n Direction as DirectionPrimitive,\n Slider as SliderPrimitive,\n Slot as SlotPrimitive,\n} from \"radix-ui\";\nimport * as React from \"react\";\nimport { useComposedRefs } from \"@/lib/compose-refs\";\nimport { cn } from \"@/lib/utils\";\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\";\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;\ntype AreaElement = React.ComponentRef;\ntype InputElement = React.ComponentRef;\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(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(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(\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 Omit,\n Pick<\n React.ComponentProps,\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(() => {\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(() => {\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 \n \n \n );\n}\n\ninterface ColorPickerImplProps\n 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(\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(\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 \n \n {isFormControl && (\n \n )}\n \n );\n }\n\n return (\n \n \n \n {isFormControl && (\n \n )}\n \n \n );\n}\n\nfunction ColorPickerTrigger(\n props: React.ComponentProps,\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 \n \n \n );\n}\n\nfunction ColorPickerContent(\n props: React.ComponentProps,\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 \n {children}\n \n );\n }\n\n return (\n \n {children}\n \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(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) => {\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) => {\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) => {\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 \n
\n \n \n \n
\n \n \n );\n}\n\nfunction ColorPickerHueSlider(\n props: React.ComponentProps,\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 \n \n \n \n \n \n );\n}\n\nfunction ColorPickerAlphaSlider(\n props: React.ComponentProps,\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 \n \n \n \n \n \n \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 \n );\n}\n\nfunction ColorPickerEyeDropper(props: React.ComponentProps) {\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 \n {children ?? }\n \n );\n}\n\ninterface ColorPickerFormatSelectProps\n extends Omit, \"value\" | \"onValueChange\">,\n Pick, \"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 \n \n \n \n \n {colorFormats.map((format) => (\n \n {format.toUpperCase()}\n \n ))}\n \n \n );\n}\n\ninterface ColorPickerInputProps\n extends Omit<\n React.ComponentProps,\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 \n );\n }\n\n if (format === \"rgb\") {\n return (\n \n );\n }\n\n if (format === \"hsl\") {\n return (\n \n );\n }\n\n if (format === \"hsb\") {\n return (\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 React.ComponentProps,\n VariantProps {}\n\nfunction InputGroupItem({\n className,\n position,\n ...props\n}: InputGroupItemProps) {\n return (\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) => {\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) => {\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 \n );\n }\n\n return (\n \n \n \n \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) => {\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 \n \n \n \n {!withoutAlpha && (\n \n )}\n \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) => {\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) => {\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 \n \n \n \n {!withoutAlpha && (\n \n )}\n \n );\n}\n\ninterface HsbInputProps extends Omit {\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) => {\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) => {\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 \n \n \n \n {!withoutAlpha && (\n \n )}\n \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\n extends Omit<\n React.InputHTMLAttributes,\n \"value\" | \"checked\" | \"onReset\"\n > {\n value?: T;\n checked?: boolean;\n control: HTMLElement | null;\n bubbles?: boolean;\n}\n\nfunction VisuallyHiddenInput(\n props: VisuallyHiddenInputProps,\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(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(() => {\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 \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 = React.Ref | 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(ref: PossibleRef, 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(...refs: PossibleRef[]): React.RefCallback {\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(...refs: PossibleRef[]): React.RefCallback {\n // biome-ignore lint/correctness/useExhaustiveDependencies: we want to memoize by all values\n return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { composeRefs, useComposedRefs };\n", "target": "" } ] }