#3b82f6
"use client"
import * as React from "react"
import { ColorPicker } from "@/components/ui/color-picker"
export default function ColorPickerDemo() {
const [color, setColor] = React.useState("#3b82f6")
return (
<div className="flex flex-col items-center gap-3">
<ColorPicker value={color} onValueChange={setColor} />
<p className="text-muted-foreground font-mono text-sm uppercase">
{color}
</p>
</div>
)
}Installation
npx logic2b@latest add color-pickerWorking with an AI assistant? Copy this prompt into Claude Code, Cursor or Copilot — it carries the install steps, a no-CLI fallback and a verification checklist.
Add the "color-picker" item from the logic2b ui registry (https://ui.logic2b.com) to this project. Follow every step and verify at the end. ## Steps 1. If the project has no `components.json` yet, set up logic2b ui first: ```bash npx logic2b@latest init ``` 2. Install the item (the CLI resolves registry dependencies automatically): ```bash npx logic2b@latest add color-picker ``` 3. Install any npm dependencies the command prints. ## If the CLI is not available Do it manually from the registry payload: 1. Fetch https://ui.logic2b.com/r/color-picker.json. 2. Write every entry in `files[]` to its mapped project path (see below). 3. Repeat (recursively) for each name in `registryDependencies`. 4. Install every package listed in `dependencies` across those payloads. ## Registry reference - Index of every item: https://ui.logic2b.com/r/index.json - Item payload (full source): https://ui.logic2b.com/r/<name>.json - Usage examples per item: https://ui.logic2b.com/r/demos/index.json → https://ui.logic2b.com/r/demos/<demo>.json - Docs index for agents: https://ui.logic2b.com/llms.txt (full docs: https://ui.logic2b.com/llms-full.txt) - Every docs page is Markdown when you append `.md` to its URL. Registry file paths map to project paths like this: - `ui/*` → `@/components/ui/*` - `blocks/<name>/*` → `@/components/*` - `charts/*` → `@/components/charts/*` - `hooks/*` → `@/hooks/*` - `lib/utils.ts` → `@/lib/utils.ts` - `theme.css` → next to the project's Tailwind entry stylesheet ## Conventions (do not break these) - Tailwind CSS v4 (CSS-first config; no tailwind.config.js needed) and React 19. - All colors go through semantic tokens (`bg-primary`, `text-muted-foreground`, `border-border`, …). Never hardcode hex/oklch values in components. - Dark mode is class-based: toggle `.dark` on `<html>`. - Icons come from lucide-react. ## Verify before finishing 1. The dev server starts and the app renders without console errors. 2. TypeScript compiles (`tsc --noEmit` or the framework's check). 3. Components use the theme: a `<Button>` shows the primary token color, and toggling `.dark` on `<html>` switches the palette.
Copy into ui/color-picker.tsx:
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
/* ---------------------------------------------------------------- color math */
type Hsv = [h: number, s: number, v: number]
const clamp01 = (n: number) => Math.min(1, Math.max(0, n))
function hsvToRgb(h: number, s: number, v: number): [number, number, number] {
const c = v * s
const x = c * (1 - Math.abs(((h / 60) % 2) - 1))
const m = v - c
let r = 0
let g = 0
let b = 0
if (h < 60) [r, g, b] = [c, x, 0]
else if (h < 120) [r, g, b] = [x, c, 0]
else if (h < 180) [r, g, b] = [0, c, x]
else if (h < 240) [r, g, b] = [0, x, c]
else if (h < 300) [r, g, b] = [x, 0, c]
else [r, g, b] = [c, 0, x]
return [
Math.round((r + m) * 255),
Math.round((g + m) * 255),
Math.round((b + m) * 255),
]
}
function rgbToHex(r: number, g: number, b: number): string {
return (
"#" +
[r, g, b].map((n) => n.toString(16).padStart(2, "0")).join("")
)
}
function hexToHsv(hex: string): Hsv | null {
const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim())
if (!m) return null
const int = parseInt(m[1], 16)
let r = (int >> 16) & 255
let g = (int >> 8) & 255
let b = int & 255
r /= 255
g /= 255
b /= 255
const max = Math.max(r, g, b)
const min = Math.min(r, g, b)
const d = max - min
let h = 0
if (d) {
if (max === r) h = ((g - b) / d) % 6
else if (max === g) h = (b - r) / d + 2
else h = (r - g) / d + 4
h *= 60
if (h < 0) h += 360
}
return [h, max === 0 ? 0 : d / max, max]
}
const hsvToHex = ([h, s, v]: Hsv) => rgbToHex(...hsvToRgb(h, s, v))
/* ------------------------------------------------------------------ component */
export interface ColorPickerProps
extends Omit<React.ComponentProps<"div">, "onChange" | "defaultValue"> {
/** Current color as a `#rrggbb` hex string (controlled). */
value?: string
defaultValue?: string
onValueChange?: (hex: string) => void
}
/**
* A self-contained HSV color picker: a saturation/value area, a hue slider and
* a hex input. Controlled via `value`/`onValueChange` or uncontrolled via
* `defaultValue`. Emits `#rrggbb`. Zero dependencies.
*/
function ColorPicker({
className,
value,
defaultValue,
onValueChange,
...props
}: ColorPickerProps) {
const [hsv, setHsv] = React.useState<Hsv>(
() => hexToHsv(value ?? defaultValue ?? "#000000") ?? [0, 0, 0]
)
const hex = hsvToHex(hsv)
const lastEmit = React.useRef(hex)
const [hexDraft, setHexDraft] = React.useState(hex)
// Sync from a controlled value that changed outside our own emissions.
React.useEffect(() => {
if (value && value.toLowerCase() !== lastEmit.current.toLowerCase()) {
const parsed = hexToHsv(value)
if (parsed) {
setHsv(parsed)
setHexDraft(hsvToHex(parsed))
}
}
}, [value])
const commit = (next: Hsv) => {
setHsv(next)
const h = hsvToHex(next)
setHexDraft(h)
lastEmit.current = h
onValueChange?.(h)
}
const [hue, sat, val] = hsv
const svRef = React.useRef<HTMLDivElement>(null)
const hueRef = React.useRef<HTMLDivElement>(null)
const moveSv = (e: React.PointerEvent) => {
const rect = svRef.current?.getBoundingClientRect()
if (!rect) return
commit([
hue,
clamp01((e.clientX - rect.left) / rect.width),
clamp01(1 - (e.clientY - rect.top) / rect.height),
])
}
const moveHue = (e: React.PointerEvent) => {
const rect = hueRef.current?.getBoundingClientRect()
if (!rect) return
commit([clamp01((e.clientX - rect.left) / rect.width) * 360, sat, val])
}
const startDrag =
(handler: (e: React.PointerEvent) => void) => (e: React.PointerEvent) => {
e.currentTarget.setPointerCapture(e.pointerId)
handler(e)
}
const hueHex = hsvToHex([hue, 1, 1])
return (
<div
data-slot="color-picker"
className={cn("flex w-56 flex-col gap-3", className)}
{...props}
>
{/* Saturation / value area */}
<div
ref={svRef}
data-slot="color-picker-area"
onPointerDown={startDrag(moveSv)}
onPointerMove={(e) => e.buttons === 1 && moveSv(e)}
className="relative h-40 w-full cursor-crosshair touch-none overflow-hidden rounded-md border"
style={{
backgroundColor: hueHex,
backgroundImage:
"linear-gradient(to top, #000, transparent), linear-gradient(to right, #fff, transparent)",
}}
>
<span
className="pointer-events-none absolute size-3 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white shadow-sm ring-1 ring-black/30"
style={{ left: `${sat * 100}%`, top: `${(1 - val) * 100}%` }}
/>
</div>
{/* Hue slider */}
<div
ref={hueRef}
data-slot="color-picker-hue"
onPointerDown={startDrag(moveHue)}
onPointerMove={(e) => e.buttons === 1 && moveHue(e)}
className="relative h-3 w-full cursor-pointer touch-none rounded-full border"
style={{
backgroundImage:
"linear-gradient(to right, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00)",
}}
>
<span
className="pointer-events-none absolute top-1/2 size-4 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white shadow-sm ring-1 ring-black/30"
style={{ left: `${(hue / 360) * 100}%` }}
/>
</div>
{/* Swatch + hex input */}
<div className="flex items-center gap-2">
<span
data-slot="color-picker-swatch"
className="size-8 shrink-0 rounded-md border"
style={{ backgroundColor: hex }}
/>
<input
data-slot="color-picker-hex"
aria-label="Hex color"
value={hexDraft}
spellCheck={false}
onChange={(e) => {
const next = e.target.value
setHexDraft(next)
const parsed = hexToHsv(next)
if (parsed) commit(parsed)
}}
onBlur={() => setHexDraft(hex)}
className={cn(
"border-input h-8 w-full min-w-0 rounded-md border bg-transparent px-2 font-mono text-sm uppercase shadow-xs outline-none",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]"
)}
/>
</div>
</div>
)
}
export { ColorPicker }Usage
import { ColorPicker } from "@/components/ui/color-picker"
const [color, setColor] = React.useState("#3b82f6")
<ColorPicker value={color} onValueChange={setColor} />
Drag inside the square to set saturation and brightness, drag the hue bar to set
the hue, or type a hex value directly. It emits a #rrggbb string and is fully
pointer-driven with zero dependencies.
API
| Prop | Type | Default |
|---|---|---|
value |
string (#rrggbb) |
— (controlled) |
defaultValue |
string |
"#000000" |
onValueChange |
(hex: string) => void |
— |
| …props | React.ComponentProps<"div"> |
— |