import { useCallback, useRef, useState } from 'react'; import { Pressable, View } from 'react-native'; import { DownLine as ChevronDown } from 'rn-motion-ui-icons/icons/down-line'; import { useThemeColor } from '../../../theme/use-theme-color'; import { AdaptiveDropdown, type TriggerRenderProps } from '../../menus/AdaptiveDropdown/adaptive-dropdown'; import { Text } from '../../typography/Text/text'; import { type ColorSummary, type Hsv, hexToHsv, parseColor, summarize } from './hsv'; import { HueSlider } from './hue-slider'; import { SaturationPanel } from './saturation-panel'; import { Swatches } from './swatches'; // theme-exempt: the default swatch palette is preset colour data, not a theme token. const DEFAULT_SWATCHES: readonly string[] = [ '#ef4444' /* theme-exempt */, '#f97316' /* theme-exempt */, '#f59e0b' /* theme-exempt */, '#22c55e' /* theme-exempt */, '#14b8a6' /* theme-exempt */, '#06b6d4' /* theme-exempt */, '#3b82f6' /* theme-exempt */, '#8b5cf6' /* theme-exempt */, '#ec4899' /* theme-exempt */, '#64748b' /* theme-exempt */, '#1f2937' /* theme-exempt */, '#ffffff' /* theme-exempt */, ]; const FALLBACK_HSV: Hsv = { h: 0, s: 0, v: 0 }; /** The trigger: a field-like button showing the current colour and its hex. */ function ColorPickerTrigger({ hex, open, toggle, accessibilityLabel, }: TriggerRenderProps & { hex: string; accessibilityLabel?: string }) { const chevronColor = useThemeColor('muted-foreground'); return ( {hex} ); } /** The readout at the top of the panel, so the value stays visible inside the sheet. */ function Preview({ hex, rgb, hsl }: ColorSummary) { const detail = `${rgb} · ${hsl}`; return ( {hex} {detail} ); } /** What `onChange` / `onComplete` hand back — one object, every representation. */ export type ColorPickerChange = ColorSummary; export type ColorPickerProps = { /** Current color as a hex / `rgb()` / `hsl()` string. Omit to let the picker own its state. */ value?: string; /** Initial color when uncontrolled. @default '#000000' */ defaultValue?: string; /** Fires on every change, including while dragging the panel or slider. */ onChange?: (color: ColorPickerChange) => void; /** Fires once when a drag ends or a swatch is tapped. */ onComplete?: (color: ColorPickerChange) => void; /** Preset colors in the swatch grid. @default a curated 12-color palette */ swatches?: readonly string[]; /** Header title inside the panel. */ title?: string; /** Which edge of the trigger the panel aligns to. @default 'start' */ align?: 'start' | 'end'; /** Panel width in pixels (wide screen only). */ width?: number; /** When true, the bottom sheet on small screens stretches to full height. @default false */ fullSheet?: boolean; testID?: string; accessibilityLabel?: string; }; /** * A colour picker that opens inside an {@link AdaptiveDropdown}: a floating * panel on wide screens, a bottom sheet on small ones. The panel composes an * HSV saturation/value plane, a hue slider and a preset swatch grid — the same * anatomy as `reanimated-color-picker`, rebuilt on the package's own * PanResponder + `react-native-svg` primitives so it needs no extra dependency. */ export function ColorPicker({ value, defaultValue = '#000000' /* theme-exempt: initial colour value, not a theme token */, onChange, onComplete, swatches = DEFAULT_SWATCHES, title, align = 'start', width, fullSheet = false, testID, accessibilityLabel, }: ColorPickerProps) { const [internal, setInternal] = useState(() => parseColor(value ?? defaultValue) ?? FALLBACK_HSV); const controlled = value !== undefined; const hsv = controlled ? (parseColor(value) ?? FALLBACK_HSV) : internal; const summary = summarize(hsv); // The latest HSV in a ref so the drag callbacks below never capture a stale // hue: dragging the saturation plane changes only S/V, and the merge must read // the hue the *other* control last committed, not whatever this render bound. const hsvRef = useRef(hsv); hsvRef.current = hsv; const commit = useCallback( (patch: Partial) => { const next = { ...hsvRef.current, ...patch }; if (!controlled) setInternal(next); onChange?.(summarize(next)); }, [controlled, onChange], ); const finish = useCallback( (patch: Partial) => { onComplete?.(summarize({ ...hsvRef.current, ...patch })); }, [onComplete], ); const selectSwatch = useCallback( (hex: string) => { const next = hexToHsv(hex) ?? FALLBACK_HSV; if (!controlled) setInternal(next); onChange?.(summarize(next)); onComplete?.(summarize(next)); }, [controlled, onChange, onComplete], ); const onSaturationChange = useCallback((s: number, v: number) => commit({ s, v }), [commit]); const onSaturationComplete = useCallback((s: number, v: number) => finish({ s, v }), [finish]); const onHueChange = useCallback((h: number) => commit({ h }), [commit]); const onHueComplete = useCallback((h: number) => finish({ h }), [finish]); const renderTrigger = useCallback( (props: TriggerRenderProps) => , [summary.hex, accessibilityLabel], ); const root = testID ?? 'color-picker'; return ( ); }