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 (
);
}