# Color Picker

> A color picker component with HSV color space selection and swatch display.

**BNA UI** — a React Native / Expo component library.
These components render through `react-native`, not the DOM: there are no HTML
elements, no Tailwind classes and no Radix primitives. Source is copied into your
project and imported through `@/components/ui/*`, `@/components/charts/*`,
`@/hooks/*` and `@/theme/*`. Colours come from the `useColor` hook rather than
hardcoded hex; sizing tokens (`HEIGHT`, `FONT_SIZE`, `BORDER_RADIUS`, `CORNERS`)
come from `@/theme/globals`.

- Docs: https://ui.ahmedbna.com/docs/components/color-picker
- Markdown: https://ui.ahmedbna.com/docs/components/color-picker.md
- Structured JSON (props, usage, source, examples): https://ui.ahmedbna.com/r/ai/color-picker.json
- Install payload (source plus every file it imports): https://ui.ahmedbna.com/r/color-picker.json
- Install: `npx bna-ui add color-picker`
- npm dependencies: `expo-linear-gradient`, `react-native-gesture-handler`, `react-native-reanimated`, `react-native-svg`, `react-native-worklets`
- Registry dependencies: `mode-provider`, `useColorScheme`, `colors`, `useColor`, `globals`, `text`, `view`
- Preview recording: https://demo.ahmedbna.com/0115-color-picker-demo.MP4

---

**Example:** A basic color picker with swatch and modal selection

```tsx
// components/demo/color-picker/color-picker-demo.tsx
import { ColorPicker } from '@/components/ui/color-picker';
import React, { useState } from 'react';

export function ColorPickerDemo() {
  const [color, setColor] = useState('#ff0000');

  return (
    <ColorPicker
      value={color}
      onColorChange={setColor}
      onColorSelect={(selectedColor) => {
        console.log('Color selected:', selectedColor);
        setColor(selectedColor);
      }}
    />
  );
}
```

## Installation

### CLI

```bash
npx bna-ui add color-picker
```

### Manual

**1.** Install the following dependencies:

```bash
npx expo install expo-linear-gradient react-native-gesture-handler react-native-reanimated react-native-svg
```

**2.** Copy and paste the following code into your project.

```tsx
// components/ui/color-picker.tsx
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import { useColor } from '@/hooks/useColor';
import { BORDER_RADIUS, CORNERS, FONT_SIZE, HEIGHT } from '@/theme/globals';
import { LinearGradient as ExpoLinearGradient } from 'expo-linear-gradient';
import React, { useCallback, useEffect, useState } from 'react';
import {
  Dimensions,
  Modal,
  StyleSheet,
  TextInput,
  TouchableOpacity,
  ViewStyle,
} from 'react-native';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
  runOnJS,
  useAnimatedStyle,
  useSharedValue,
} from 'react-native-reanimated';
import Svg, { Defs, LinearGradient, Rect, Stop } from 'react-native-svg';

const { width: screenWidth } = Dimensions.get('window');
const PICKER_SIZE = screenWidth - 40;
const HUE_BAR_HEIGHT = 40;
const KNOB_SIZE = 40;

// Color utility functions
const hsvToRgb = (h: number, s: number, v: number) => {
  const c = v * s;
  const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
  const m = v - c;
  let r = 0,
    g = 0,
    b = 0;

  if (0 <= h && h < 60) {
    r = c;
    g = x;
    b = 0;
  } else if (60 <= h && h < 120) {
    r = x;
    g = c;
    b = 0;
  } else if (120 <= h && h < 180) {
    r = 0;
    g = c;
    b = x;
  } else if (180 <= h && h < 240) {
    r = 0;
    g = x;
    b = c;
  } else if (240 <= h && h < 300) {
    r = x;
    g = 0;
    b = c;
  } else if (300 <= h && h < 360) {
    r = c;
    g = 0;
    b = x;
  }

  return {
    r: Math.round((r + m) * 255),
    g: Math.round((g + m) * 255),
    b: Math.round((b + m) * 255),
  };
};

const rgbToHex = (r: number, g: number, b: number) => {
  return `#${r.toString(16).padStart(2, '0')}${g
    .toString(16)
    .padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
};

const hexToRgb = (hex: string) => {
  let normalized = hex.replace('#', '');
  if (normalized.length === 3) {
    normalized = normalized
      .split('')
      .map((c) => c + c)
      .join('');
  } else if (normalized.length === 8) {
    // Drop the alpha channel — this picker has no alpha concept downstream.
    normalized = normalized.slice(0, 6);
  }
  const result = /^([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(normalized);
  return result
    ? {
        r: parseInt(result[1], 16),
        g: parseInt(result[2], 16),
        b: parseInt(result[3], 16),
      }
    : { r: 0, g: 0, b: 0 };
};

const isValidHex = (hex: string) =>
  /^#?([a-f\d]{3}|[a-f\d]{6}|[a-f\d]{8})$/i.test(hex);

const rgbToHsv = (r: number, g: number, b: number) => {
  r /= 255;
  g /= 255;
  b /= 255;

  const max = Math.max(r, g, b);
  const min = Math.min(r, g, b);
  const diff = max - min;

  let h = 0;
  if (diff !== 0) {
    if (max === r) {
      h = ((g - b) / diff) % 6;
    } else if (max === g) {
      h = (b - r) / diff + 2;
    } else {
      h = (r - g) / diff + 4;
    }
  }
  h = Math.round(h * 60);
  if (h < 0) h += 360;

  const s = max === 0 ? 0 : diff / max;
  const v = max;

  return { h, s, v };
};

interface ColorSwatchProps {
  color: string;
  size?: number;
  style?: ViewStyle;
  onPress?: () => void;
  accessibilityLabel?: string;
}

export const ColorSwatch: React.FC<ColorSwatchProps> = ({
  color,
  size = 32,
  style,
  onPress,
  accessibilityLabel,
}) => {
  const borderColor = useColor('border');

  return (
    <TouchableOpacity
      onPress={onPress}
      style={[
        {
          width: size,
          height: size,
          borderRadius: size / 2,
          backgroundColor: color,
          borderWidth: 2,
          borderColor,
        },
        style,
      ]}
      activeOpacity={onPress ? 0.8 : 1}
      accessibilityRole={onPress ? 'button' : 'image'}
      accessibilityLabel={accessibilityLabel ?? `Color swatch ${color}`}
    />
  );
};

interface ColorPickerProps {
  value?: string;
  onColorChange?: (color: string) => void;
  onColorSelect?: (color: string) => void;
  swatchSize?: number;
  disabled?: boolean;
  style?: ViewStyle;
}

export const ColorPicker: React.FC<ColorPickerProps> = ({
  value = '#ff0000',
  onColorChange,
  onColorSelect,
  swatchSize = HEIGHT,
  disabled = false,
  style,
}) => {
  const [isModalVisible, setIsModalVisible] = useState(false);
  const [currentColor, setCurrentColor] = useState(value);
  const [pureHueColor, setPureHueColor] = useState('#ff0000');
  const [hexInput, setHexInput] = useState(value);

  const backgroundColor = useColor('background');
  const card = useColor('card');
  const borderColor = useColor('border');
  const textColor = useColor('text');

  // Initialize HSV values from the current color
  const rgb = hexToRgb(currentColor);
  const initialHsv = rgbToHsv(rgb.r, rgb.g, rgb.b);

  const hue = useSharedValue(initialHsv.h);
  const saturation = useSharedValue(initialHsv.s);
  const brightness = useSharedValue(initialHsv.v);

  const updateColor = useCallback(
    (h: number, s: number, v: number) => {
      const rgb = hsvToRgb(h, s, v);
      const hex = rgbToHex(rgb.r, rgb.g, rgb.b);
      setCurrentColor(hex);
      onColorChange?.(hex);

      // Update pure hue color for the saturation/brightness picker
      const pureRgb = hsvToRgb(h, 1, 1);
      const pureHex = rgbToHex(pureRgb.r, pureRgb.g, pureRgb.b);
      setPureHueColor(pureHex);
    },
    [onColorChange]
  );

  // Update pure hue color when modal opens
  useEffect(() => {
    if (isModalVisible) {
      const rgb = hexToRgb(currentColor);
      const hsv = rgbToHsv(rgb.r, rgb.g, rgb.b);
      hue.value = hsv.h;
      saturation.value = hsv.s;
      brightness.value = hsv.v;

      const pureRgb = hsvToRgb(hsv.h, 1, 1);
      const pureHex = rgbToHex(pureRgb.r, pureRgb.g, pureRgb.b);
      setPureHueColor(pureHex);
      setHexInput(currentColor);
    }
  }, [isModalVisible, currentColor]);

  // Dragging previously called runOnJS(updateColor) on every pan frame,
  // forcing a full JS re-render per touch-move. Live visuals (preview swatch
  // + knob positions) now derive purely from shared values on the UI
  // thread; the JS-side state commit (currentColor, onColorChange) happens
  // once, on gesture end.
  const hueGesture = Gesture.Pan()
    .onUpdate((event) => {
      const newX = Math.max(
        0,
        Math.min(PICKER_SIZE - KNOB_SIZE, event.x - KNOB_SIZE / 2)
      );
      hue.value = (newX / (PICKER_SIZE - KNOB_SIZE)) * 360;
    })
    .onEnd(() => {
      runOnJS(updateColor)(hue.value, saturation.value, brightness.value);
    });

  // Saturation/Brightness picker gesture using new Gesture API
  const pickerGesture = Gesture.Pan()
    .onUpdate((event) => {
      const newX = Math.max(
        0,
        Math.min(PICKER_SIZE - KNOB_SIZE, event.x - KNOB_SIZE / 2)
      );
      const newY = Math.max(
        0,
        Math.min(PICKER_SIZE - KNOB_SIZE, event.y - KNOB_SIZE / 2)
      );

      saturation.value = newX / (PICKER_SIZE - KNOB_SIZE);
      brightness.value = 1 - newY / (PICKER_SIZE - KNOB_SIZE);
    })
    .onEnd(() => {
      runOnJS(updateColor)(hue.value, saturation.value, brightness.value);
    });

  const hueKnobStyle = useAnimatedStyle(() => ({
    transform: [{ translateX: (hue.value / 360) * (PICKER_SIZE - KNOB_SIZE) }],
  }));

  const pickerKnobStyle = useAnimatedStyle(() => ({
    transform: [
      { translateX: saturation.value * (PICKER_SIZE - KNOB_SIZE) },
      { translateY: (1 - brightness.value) * (PICKER_SIZE - KNOB_SIZE) },
    ],
  }));

  // Live preview swatch, driven entirely on the UI thread so dragging never
  // triggers a JS re-render — hsvToRgb is auto-workletized by the Reanimated
  // Babel plugin since it's referenced from inside this worklet.
  const previewAnimatedStyle = useAnimatedStyle(() => {
    const rgb = hsvToRgb(hue.value, saturation.value, brightness.value);
    return {
      backgroundColor: `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`,
    };
  });

  const handleHexSubmit = useCallback(() => {
    if (!isValidHex(hexInput)) {
      setHexInput(currentColor);
      return;
    }

    const normalized = hexInput.startsWith('#') ? hexInput : `#${hexInput}`;
    const rgb = hexToRgb(normalized);
    const hsv = rgbToHsv(rgb.r, rgb.g, rgb.b);
    hue.value = hsv.h;
    saturation.value = hsv.s;
    brightness.value = hsv.v;
    updateColor(hsv.h, hsv.s, hsv.v);
  }, [hexInput, currentColor, updateColor]);

  const handleColorSelect = () => {
    onColorSelect?.(currentColor);
    setIsModalVisible(false);
  };

  const handleCancel = () => {
    setCurrentColor(value);
    setIsModalVisible(false);
  };

  return (
    <View style={style}>
      <ColorSwatch
        color={currentColor}
        size={swatchSize}
        onPress={disabled ? undefined : () => setIsModalVisible(true)}
        accessibilityLabel={`Selected color ${currentColor}. Opens color picker.`}
      />

      <Modal
        visible={isModalVisible}
        animationType='slide'
        presentationStyle='pageSheet'
        onRequestClose={handleCancel}
      >
        <View style={[styles.modalContainer, { backgroundColor: card }]}>
          <View style={styles.header}>
            <TouchableOpacity onPress={handleCancel}>
              <Text>Cancel</Text>
            </TouchableOpacity>
            <Text variant='title'>Choose Color</Text>
            <TouchableOpacity onPress={handleColorSelect}>
              <Text>Done</Text>
            </TouchableOpacity>
          </View>

          <View style={styles.content}>
            {/* Color Preview */}
            <Animated.View
              style={[
                {
                  height: 80,
                  width: '100%',
                  alignItems: 'center',
                  marginBottom: 16,
                  justifyContent: 'center',
                  borderRadius: BORDER_RADIUS,
                },
                previewAnimatedStyle,
              ]}
              accessibilityLabel={`Color preview: ${currentColor}`}
            >
              <Text
                variant='caption'
                style={{ color: textColor, fontWeight: 'bold' }}
              >
                {currentColor.toUpperCase()}
              </Text>
            </Animated.View>

            {/* Manual hex entry — the drag-only pickers below are not
                operable via VoiceOver/TalkBack/switch control, so this is
                the accessible path to picking an exact color. */}
            <TextInput
              value={hexInput}
              onChangeText={setHexInput}
              onSubmitEditing={handleHexSubmit}
              onBlur={handleHexSubmit}
              autoCapitalize='none'
              autoCorrect={false}
              maxLength={9}
              placeholder='#RRGGBB'
              placeholderTextColor={textColor}
              accessibilityLabel='Hex color code'
              accessibilityHint='Enter a hex color code, for example RRGGBB'
              style={{
                width: '100%',
                marginBottom: 24,
                paddingHorizontal: 16,
                height: HEIGHT,
                borderRadius: CORNERS,
                borderWidth: 1,
                borderColor,
                color: textColor,
                fontSize: FONT_SIZE,
                textAlign: 'center',
              }}
            />

            {/* Saturation/Brightness Picker */}
            <View style={styles.pickerContainer}>
              <GestureDetector gesture={pickerGesture}>
                <View style={{ width: PICKER_SIZE, height: PICKER_SIZE }}>
                  {/* Base color layer */}
                  <View
                    style={[
                      styles.colorBase,
                      { backgroundColor: pureHueColor },
                    ]}
                  />

                  {/* Saturation gradient (white to transparent, left to right) */}
                  <ExpoLinearGradient
                    colors={['rgba(255,255,255,1)', 'rgba(255,255,255,0)']}
                    start={{ x: 0, y: 0 }}
                    end={{ x: 1, y: 0 }}
                    style={styles.gradientLayer}
                  />

                  {/* Brightness gradient (transparent to black, top to bottom) */}
                  <ExpoLinearGradient
                    colors={['rgba(0,0,0,0)', 'rgba(0,0,0,1)']}
                    start={{ x: 0, y: 0 }}
                    end={{ x: 0, y: 1 }}
                    style={styles.gradientLayer}
                  />

                  <Animated.View style={[styles.pickerKnob, pickerKnobStyle]} />
                </View>
              </GestureDetector>
            </View>

            {/* Hue Bar */}
            <View style={styles.hueContainer}>
              <GestureDetector gesture={hueGesture}>
                <Animated.View>
                  <Svg width={PICKER_SIZE} height={HUE_BAR_HEIGHT}>
                    <Defs>
                      <LinearGradient
                        id='hue'
                        x1='0%'
                        y1='0%'
                        x2='100%'
                        y2='0%'
                      >
                        <Stop offset='0%' stopColor='#ff0000' />
                        <Stop offset='16.66%' stopColor='#ffff00' />
                        <Stop offset='33.33%' stopColor='#00ff00' />
                        <Stop offset='50%' stopColor='#00ffff' />
                        <Stop offset='66.66%' stopColor='#0000ff' />
                        <Stop offset='83.33%' stopColor='#ff00ff' />
                        <Stop offset='100%' stopColor='#ff0000' />
                      </LinearGradient>
                    </Defs>
                    <Rect
                      width={PICKER_SIZE}
                      height={HUE_BAR_HEIGHT}
                      fill='url(#hue)'
                      rx={10}
                    />
                  </Svg>

                  <Animated.View style={[styles.hueKnob, hueKnobStyle]} />
                </Animated.View>
              </GestureDetector>
            </View>
          </View>
        </View>
      </Modal>
    </View>
  );
};

const styles = StyleSheet.create({
  modalContainer: {
    flex: 1,
  },
  header: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingHorizontal: 20,
    paddingVertical: 16,
  },
  content: {
    flex: 1,
    padding: 20,
    alignItems: 'center',
  },
  pickerContainer: {
    marginBottom: 30,
    borderRadius: BORDER_RADIUS,
    overflow: 'hidden',
  },
  colorBase: {
    position: 'absolute',
    width: PICKER_SIZE,
    height: PICKER_SIZE,
    borderRadius: BORDER_RADIUS,
  },
  gradientLayer: {
    position: 'absolute',
    width: PICKER_SIZE,
    height: PICKER_SIZE,
    borderRadius: 12,
  },
  pickerKnob: {
    position: 'absolute',
    width: KNOB_SIZE,
    height: KNOB_SIZE,
    borderRadius: CORNERS,
    backgroundColor: 'white',
    borderWidth: 1,
    borderColor: '#000',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.3,
    shadowRadius: 4,
    elevation: 5,
  },
  hueContainer: {
    borderRadius: CORNERS,
    overflow: 'hidden',
  },
  hueKnob: {
    position: 'absolute',
    width: KNOB_SIZE,
    height: KNOB_SIZE,
    borderRadius: CORNERS,
    backgroundColor: 'white',
    opacity: 0.5,
    borderWidth: 2,
    borderColor: '#000',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.3,
    shadowRadius: 4,
    elevation: 5,
  },
});
```

**3.** Update the import paths to match your project setup.

## Usage

```tsx
import { ColorPicker, ColorSwatch } from '@/components/ui/color-picker';
```

```tsx
<ColorPicker
  value='#ff0000'
  onColorChange={(color) => console.log('Color changed:', color)}
  onColorSelect={(color) => console.log('Color selected:', color)}
/>
```

## Examples

#### Default

**Example:** A basic color picker with swatch and modal selection

```tsx
// components/demo/color-picker/color-picker-demo.tsx
import { ColorPicker } from '@/components/ui/color-picker';
import React, { useState } from 'react';

export function ColorPickerDemo() {
  const [color, setColor] = useState('#ff0000');

  return (
    <ColorPicker
      value={color}
      onColorChange={setColor}
      onColorSelect={(selectedColor) => {
        console.log('Color selected:', selectedColor);
        setColor(selectedColor);
      }}
    />
  );
}
```

#### Different Sizes

**Example:** Color pickers with different swatch sizes

```tsx
// components/demo/color-picker/color-picker-sizes.tsx
import { ColorPicker } from '@/components/ui/color-picker';
import { View } from '@/components/ui/view';
import React, { useState } from 'react';

export function ColorPickerSizes() {
  const [smallColor, setSmallColor] = useState('#ff6b6b');
  const [mediumColor, setMediumColor] = useState('#4ecdc4');
  const [largeColor, setLargeColor] = useState('#45b7d1');
  const [xlColor, setXlColor] = useState('#f9ca24');

  return (
    <View style={{ flexDirection: 'row', alignItems: 'center', gap: 16 }}>
      <ColorPicker
        value={smallColor}
        onColorChange={setSmallColor}
        onColorSelect={setSmallColor}
        swatchSize={24}
      />
      <ColorPicker
        value={mediumColor}
        onColorChange={setMediumColor}
        onColorSelect={setMediumColor}
        swatchSize={32}
      />
      <ColorPicker
        value={largeColor}
        onColorChange={setLargeColor}
        onColorSelect={setLargeColor}
        swatchSize={48}
      />
      <ColorPicker
        value={xlColor}
        onColorChange={setXlColor}
        onColorSelect={setXlColor}
        swatchSize={64}
      />
    </View>
  );
}
```

#### With Initial Colors

**Example:** Color pickers with different initial colors

```tsx
// components/demo/color-picker/color-picker-colors.tsx
import { ColorPicker } from '@/components/ui/color-picker';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import React, { useState } from 'react';

export function ColorPickerColors() {
  const [redColor, setRedColor] = useState('#e74c3c');
  const [greenColor, setGreenColor] = useState('#2ecc71');
  const [blueColor, setBlueColor] = useState('#3498db');
  const [purpleColor, setPurpleColor] = useState('#9b59b6');
  const [orangeColor, setOrangeColor] = useState('#f39c12');

  const colorData = [
    { name: 'Red', color: redColor, setter: setRedColor },
    { name: 'Green', color: greenColor, setter: setGreenColor },
    { name: 'Blue', color: blueColor, setter: setBlueColor },
    { name: 'Purple', color: purpleColor, setter: setPurpleColor },
    { name: 'Orange', color: orangeColor, setter: setOrangeColor },
  ];

  return (
    <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 16 }}>
      {colorData.map(({ name, color, setter }) => (
        <View key={name} style={{ alignItems: 'center', gap: 8 }}>
          <ColorPicker
            value={color}
            onColorChange={setter}
            onColorSelect={setter}
            swatchSize={40}
          />
          <Text variant='caption'>{name}</Text>
        </View>
      ))}
    </View>
  );
}
```

#### Disabled State

**Example:** Disabled color picker that cannot be opened

```tsx
// components/demo/color-picker/color-picker-disabled.tsx
import { ColorPicker } from '@/components/ui/color-picker';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import React from 'react';

export function ColorPickerDisabled() {
  return (
    <View style={{ alignItems: 'center', gap: 12 }}>
      <ColorPicker value='#6c757d' disabled={true} swatchSize={48} />
      <Text variant='caption' style={{ opacity: 0.6 }}>
        Disabled Color Picker
      </Text>
    </View>
  );
}
```

#### Color Swatch Only

**Example:** Standalone color swatches without picker functionality

```tsx
// components/demo/color-picker/color-swatch-demo.tsx
import { ColorSwatch } from '@/components/ui/color-picker';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import React, { useState } from 'react';

export function ColorSwatchDemo() {
  const [selectedColor, setSelectedColor] = useState('#e74c3c');

  const colors = [
    '#e74c3c',
    '#3498db',
    '#2ecc71',
    '#f39c12',
    '#9b59b6',
    '#1abc9c',
    '#e67e22',
    '#34495e',
  ];

  return (
    <View style={{ alignItems: 'center', gap: 16 }}>
      <View
        style={{
          flexDirection: 'row',
          flexWrap: 'wrap',
          gap: 8,
          justifyContent: 'center',
        }}
      >
        {colors.map((color) => (
          <ColorSwatch
            key={color}
            color={color}
            size={36}
            onPress={() => setSelectedColor(color)}
            style={{
              borderWidth: selectedColor === color ? 3 : 2,
              borderColor: selectedColor === color ? '#000' : 'transparent',
            }}
          />
        ))}
      </View>
      <Text variant='body'>Selected: {selectedColor.toUpperCase()}</Text>
    </View>
  );
}
```

#### Custom Styling

**Example:** Color pickers with custom styling and layouts

```tsx
// components/demo/color-picker/color-picker-styled.tsx
import { ColorPicker } from '@/components/ui/color-picker';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import React, { useState } from 'react';

export function ColorPickerStyled() {
  const [primaryColor, setPrimaryColor] = useState('#007AFF');
  const [accentColor, setAccentColor] = useState('#FF3B30');

  return (
    <View style={{ gap: 20 }}>
      {/* Rounded Square Style */}
      <View style={{ alignItems: 'center', gap: 8 }}>
        <ColorPicker
          value={primaryColor}
          onColorChange={setPrimaryColor}
          onColorSelect={setPrimaryColor}
          swatchSize={50}
          style={{
            shadowColor: '#000',
            shadowOffset: { width: 0, height: 2 },
            shadowOpacity: 0.25,
            shadowRadius: 4,
            elevation: 5,
          }}
        />
        <Text variant='caption'>Primary Color</Text>
      </View>

      {/* With Custom Border */}
      <View style={{ alignItems: 'center', gap: 8 }}>
        <View
          style={{
            padding: 4,
            borderRadius: 30,
            borderWidth: 2,
            borderColor: '#ddd',
            backgroundColor: '#fff',
          }}
        >
          <ColorPicker
            value={accentColor}
            onColorChange={setAccentColor}
            onColorSelect={setAccentColor}
            swatchSize={40}
          />
        </View>
        <Text variant='caption'>Accent Color</Text>
      </View>
    </View>
  );
}
```

#### Color Palette

**Example:** Multiple color pickers arranged as a color palette

```tsx
// components/demo/color-picker/color-picker-palette.tsx
import { ColorPicker } from '@/components/ui/color-picker';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import { useColor } from '@/hooks/useColor';
import React, { useState } from 'react';

export function ColorPickerPalette() {
  const cardColor = useColor('card');

  const [colors, setColors] = useState([
    '#FF6B6B',
    '#4ECDC4',
    '#45B7D1',
    '#F9CA24',
    '#6C5CE7',
    '#A29BFE',
    '#FD79A8',
    '#00B894',
  ]);

  const updateColor = (index: number, newColor: string) => {
    const newColors = [...colors];
    newColors[index] = newColor;
    setColors(newColors);
  };

  return (
    <View
      style={{
        flexDirection: 'row',
        flexWrap: 'wrap',
        gap: 12,
        justifyContent: 'center',
        padding: 16,
        backgroundColor: cardColor,
        borderRadius: 12,
      }}
    >
      {colors.map((color, index) => (
        <View key={index} style={{ alignItems: 'center', gap: 4 }}>
          <ColorPicker
            value={color}
            onColorChange={(newColor) => updateColor(index, newColor)}
            onColorSelect={(newColor) => updateColor(index, newColor)}
            swatchSize={36}
          />
          <Text variant='caption' style={{ fontSize: 10 }}>
            {color.toUpperCase()}
          </Text>
        </View>
      ))}
    </View>
  );
}
```

#### With Labels

**Example:** Color pickers with descriptive labels

```tsx
// components/demo/color-picker/color-picker-labeled.tsx
import { ColorPicker } from '@/components/ui/color-picker';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import React, { useState } from 'react';

export function ColorPickerLabeled() {
  const [backgroundColor, setBackgroundColor] = useState('#ffffff');
  const [textColor, setTextColor] = useState('#333333');
  const [borderColor, setBorderColor] = useState('#e1e5e9');

  return (
    <View style={{ gap: 20 }}>
      {/* Background Color */}
      <View style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>
        <ColorPicker
          value={backgroundColor}
          onColorChange={setBackgroundColor}
          onColorSelect={setBackgroundColor}
          swatchSize={40}
        />
        <View style={{ flex: 1 }}>
          <Text variant='body' style={{ fontWeight: '600' }}>
            Background Color
          </Text>
          <Text variant='caption' style={{ opacity: 0.7 }}>
            {backgroundColor.toUpperCase()}
          </Text>
        </View>
      </View>

      {/* Text Color */}
      <View style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>
        <ColorPicker
          value={textColor}
          onColorChange={setTextColor}
          onColorSelect={setTextColor}
          swatchSize={40}
        />
        <View style={{ flex: 1 }}>
          <Text variant='body' style={{ fontWeight: '600' }}>
            Text Color
          </Text>
          <Text variant='caption' style={{ opacity: 0.7 }}>
            {textColor.toUpperCase()}
          </Text>
        </View>
      </View>

      {/* Border Color */}
      <View style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>
        <ColorPicker
          value={borderColor}
          onColorChange={setBorderColor}
          onColorSelect={setBorderColor}
          swatchSize={40}
        />
        <View style={{ flex: 1 }}>
          <Text variant='body' style={{ fontWeight: '600' }}>
            Border Color
          </Text>
          <Text variant='caption' style={{ opacity: 0.7 }}>
            {borderColor.toUpperCase()}
          </Text>
        </View>
      </View>

      {/* Preview */}
      <View
        style={{
          padding: 16,
          backgroundColor: backgroundColor,
          borderWidth: 2,
          borderColor: borderColor,
          borderRadius: 8,
          marginTop: 8,
        }}
      >
        <Text style={{ color: textColor, textAlign: 'center' }}>
          Preview with selected colors
        </Text>
      </View>
    </View>
  );
}
```

## API Reference

### ColorPicker

The main color picker component that displays a swatch and opens a modal for color selection.

| Prop            | Type                      | Default     | Description                                             |
| --------------- | ------------------------- | ----------- | ------------------------------------------------------- |
| `value`         | `string`                  | `'#ff0000'` | The current color value in hex format.                  |
| `onColorChange` | `(color: string) => void` | -           | Callback fired when the color changes during selection. |
| `onColorSelect` | `(color: string) => void` | -           | Callback fired when the user confirms color selection.  |
| `swatchSize`    | `number`                  | `HEIGHT`    | The size of the color swatch in pixels.                 |
| `disabled`      | `boolean`                 | `false`     | Whether the color picker is disabled.                   |
| `style`         | `ViewStyle`               | -           | Additional styles to apply to the container.            |

### ColorSwatch

A standalone color swatch component that can be used independently.

| Prop                 | Type         | Default | Description                                                                                |
| -------------------- | ------------ | ------- | ------------------------------------------------------------------------------------------ |
| `color`              | `string`     | -       | The color to display in hex format.                                                        |
| `size`               | `number`     | `32`    | The size of the swatch in pixels.                                                          |
| `style`              | `ViewStyle`  | -       | Additional styles to apply to the swatch.                                                  |
| `onPress`            | `() => void` | -       | Callback fired when the swatch is pressed.                                                 |
| `accessibilityLabel` | `string`     | -       | Accessibility label for screen readers. Defaults to `"Color swatch {color}"` when omitted. |

## Color Utilities

The component includes several utility functions for color manipulation:

- `hsvToRgb(h, s, v)` - Converts HSV values to RGB
- `rgbToHex(r, g, b)` - Converts RGB values to hex string
- `hexToRgb(hex)` - Converts hex string to RGB values
- `rgbToHsv(r, g, b)` - Converts RGB values to HSV

## Features

- **HSV Color Space**: Uses HSV (Hue, Saturation, Value) color model for intuitive color selection
- **Interactive Gestures**: Pan gestures for hue bar and saturation/brightness picker
- **Real-time Preview**: Live color preview as you drag the selectors
- **Modal Interface**: Full-screen modal with cancel and confirm actions
- **Customizable**: Configurable swatch sizes and styling
- **Accessible**: Proper contrast and readable color values
- **Smooth Animations**: Powered by react-native-reanimated for smooth interactions

## Accessibility

The ColorPicker component is built with accessibility in mind:

- Color values are displayed in uppercase hex format for easy reading
- High contrast knobs for better visibility
- Proper touch targets for gesture interactions
- Modal provides clear cancel and confirm actions
- Color preview shows the selected color prominently

## Notes

- The component uses `expo-linear-gradient` for smooth color gradients
- Gesture handling is powered by `react-native-gesture-handler`
- Animations use `react-native-reanimated` for optimal performance
- The hue bar uses SVG gradients for precise color representation
- Color calculations maintain precision across different color spaces
