{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "color-picker", "title": "Color Picker", "description": "A hand-drawn color picker with preset swatches and editable hex input.", "dependencies": [ "roughjs" ], "registryDependencies": [ "https://www.bydefaulthuman.fun/r/use-rough.json", "utils", "https://www.bydefaulthuman.fun/r/crumble-theme.json", "https://www.bydefaulthuman.fun/r/rough-lib.json" ], "files": [ { "path": "registry/new-york/ui/color-picker.tsx", "content": "\"use client\";\n\nimport { useCallback, useContext, useEffect, useRef, useState } from \"react\";\nimport { useRough } from \"@/hooks/use-rough\";\nimport { cn } from \"@/lib/utils\";\nimport {\n CrumbleContext,\n resolveRoughVars,\n type CrumbleColorProps,\n type CrumbleTheme,\n} from \"@/lib/rough\";\n\nconst SWATCHES = [\n \"#ef4444\",\n \"#f97316\",\n \"#eab308\",\n \"#22c55e\",\n \"#06b6d4\",\n \"#3b82f6\",\n \"#8b5cf6\",\n \"#ec4899\",\n \"#ffffff\",\n \"#d1d5db\",\n \"#6b7280\",\n \"#111827\",\n];\n\nexport interface ColorPickerProps extends CrumbleColorProps {\n className?: string;\n defaultValue?: string;\n id?: string;\n label?: string;\n onChange?: (color: string) => void;\n swatches?: string[];\n theme?: CrumbleTheme;\n value?: string;\n}\n\nfunction Swatch({\n color,\n isSelected,\n onClick,\n swatchId,\n theme,\n}: {\n color: string;\n isSelected: boolean;\n onClick: () => void;\n swatchId: string;\n theme: CrumbleTheme;\n}) {\n const externalSvgRef = useRef(null);\n const SIZE = 28;\n const { drawCircle, svgRef } = useRough({\n stableId: swatchId,\n svgRef: externalSvgRef,\n theme,\n variant: isSelected ? \"interactive\" : \"border\",\n });\n\n const draw = useCallback(\n (hover = false) => {\n const svg = svgRef.current;\n if (!svg) return;\n\n svg.replaceChildren();\n svg.setAttribute(\"width\", String(SIZE));\n svg.setAttribute(\"height\", String(SIZE));\n svg.setAttribute(\"viewBox\", `0 0 ${SIZE} ${SIZE}`);\n\n if (!isSelected && !hover) return;\n\n const circle = drawCircle(SIZE / 2, SIZE / 2, SIZE - 3, {\n fill: \"none\",\n stroke: color === \"#ffffff\" ? \"#d1d5db\" : color,\n strokeWidth: isSelected ? 2 : 1,\n });\n if (circle) svg.appendChild(circle);\n },\n [color, drawCircle, isSelected, svgRef],\n );\n\n useEffect(() => {\n const id = requestAnimationFrame(() => draw());\n return () => cancelAnimationFrame(id);\n }, [draw]);\n\n return (\n draw(true)}\n onMouseLeave={() => draw(false)}\n className=\"relative rounded-full outline-none\"\n style={{ width: SIZE, height: SIZE }}\n aria-label={`Select color ${color}`}\n aria-pressed={isSelected}\n >\n \n \n \n );\n}\n\nexport function ColorPicker({\n className,\n defaultValue = \"#3b82f6\",\n fill,\n id,\n label,\n onChange,\n stroke,\n strokeMuted,\n swatches = SWATCHES,\n theme: themeProp,\n value: controlledValue,\n}: ColorPickerProps) {\n const [internalValue, setInternalValue] = useState(defaultValue);\n const [hexInput, setHexInput] = useState(defaultValue);\n const wrapperRef = useRef(null);\n const inputSvgRef = useRef(null);\n const pickerId = id ?? \"color-picker\";\n const { theme: contextTheme } = useContext(CrumbleContext);\n const theme = themeProp ?? contextTheme;\n const roughStyle = resolveRoughVars({ stroke, strokeMuted, fill });\n const { drawRect } = useRough({\n stableId: `${pickerId}-input`,\n svgRef: inputSvgRef,\n theme: themeProp,\n variant: \"border\",\n });\n\n const value = controlledValue ?? internalValue;\n\n const drawInput = useCallback(() => {\n const svg = inputSvgRef.current;\n const wrapper = wrapperRef.current;\n if (!svg || !wrapper) return;\n\n svg.replaceChildren();\n const w = wrapper.offsetWidth;\n const H = 36;\n svg.setAttribute(\"width\", String(w));\n svg.setAttribute(\"height\", String(H));\n svg.setAttribute(\"viewBox\", `0 0 ${w} ${H}`);\n\n const rect = drawRect(1, 1, w - 2, H - 2, {\n fill: \"none\",\n stroke: \"var(--cr-stroke-muted)\",\n });\n if (rect) svg.appendChild(rect);\n }, [drawRect]);\n\n useEffect(() => {\n drawInput();\n }, [drawInput]);\n\n useEffect(() => {\n const wrapper = wrapperRef.current;\n if (!wrapper) return;\n const ro = new ResizeObserver(() => drawInput());\n ro.observe(wrapper);\n return () => ro.disconnect();\n }, [drawInput]);\n\n const handleSelect = (color: string) => {\n setInternalValue(color);\n setHexInput(color);\n onChange?.(color);\n };\n\n const handleHexChange = (raw: string) => {\n setHexInput(raw);\n const cleaned = raw.startsWith(\"#\") ? raw : `#${raw}`;\n if (/^#[0-9a-f]{6}$/i.test(cleaned)) {\n setInternalValue(cleaned);\n onChange?.(cleaned);\n }\n };\n\n return (\n
\n {label ? (\n \n {label}\n \n ) : null}\n\n
\n {swatches.map((color) => (\n handleSelect(color)}\n swatchId={`${pickerId}-swatch-${color}`}\n theme={theme}\n />\n ))}\n
\n\n
\n \n
\n \n handleHexChange(e.target.value)}\n className=\"flex-1 border-none bg-transparent font-mono text-sm text-foreground outline-none\"\n aria-label=\"hex color value\"\n maxLength={7}\n />\n
\n
\n
\n );\n}\n", "type": "registry:ui", "target": "components/crumble/ui/color-picker.tsx" } ], "type": "registry:ui" }