{"version":3,"file":"color-picker-area.cjs","sources":["../../../components/color-picker/color-picker-area.tsx"],"sourcesContent":["'use client';\n\nimport { cx } from 'class-variance-authority';\nimport {\n  ComponentProps,\n  KeyboardEvent as ReactKeyboardEvent,\n  PointerEvent as ReactPointerEvent,\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef\n} from 'react';\nimport styles from './color-picker.module.css';\nimport { useColorPicker } from './color-picker-root';\nimport {\n  CHROMA_MAX,\n  clamp01,\n  hslToOklch,\n  oklchToHsl,\n  oklchToRgb\n} from './utils';\n\n// Internal pixel resolution for the C × L plane. CSS upscales this to the\n// container size; a 96² grid is the sweet spot between a smooth gradient and\n// keeping the per-hue repaint comfortably inside one frame.\nconst CANVAS_RES = 96;\n\n// Keyboard nudge sizes in the same normalized 0..1 pad space the pointer\n// uses: 1% of an axis per Arrow press, 10% for Shift+Arrow and PageUp/Down.\nconst STEP = 0.01;\nconst STEP_LARGE = 0.1;\n\nexport type ColorPickerAreaProps = ComponentProps<'div'>;\n\nexport const ColorPickerArea = (props: ColorPickerAreaProps) => {\n  const { mode } = useColorPicker();\n  return mode === 'oklch' ? <OklchArea {...props} /> : <HslArea {...props} />;\n};\n\nColorPickerArea.displayName = 'ColorPicker.Area';\n\n// OKLCH mode: chroma × lightness plane covering the full P3 gamut. Channels\n// outside sRGB are channel-clipped for display; the input remains true OKLCH.\nconst OklchArea = ({ className, ...props }: ColorPickerAreaProps) => {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const thumbRef = useRef<HTMLDivElement>(null);\n  const isDragging = useRef(false);\n  const isThumbVisible = useRef(false);\n\n  const { lightness, chroma, hue, setColor } = useColorPicker();\n  // Use the native CSS oklch() so the thumb renders the actual picked color on\n  // wide-gamut (P3) displays — hex would silently sRGB-clip wide-gamut picks.\n  const thumbColor = useMemo(\n    () => `oklch(${lightness} ${chroma} ${hue})`,\n    [lightness, chroma, hue]\n  );\n\n  // Coalesce hue-driven repaints into one per animation frame. A fast slider\n  // sweep would otherwise queue dozens of synchronous 96² repaints back-to-back.\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n    let cancelled = false;\n    const handle = requestAnimationFrame(() => {\n      if (cancelled) return;\n      const ctx = canvas.getContext('2d');\n      if (!ctx) return;\n\n      const img = ctx.createImageData(CANVAS_RES, CANVAS_RES);\n      for (let y = 0; y < CANVAS_RES; y++) {\n        const L = 1 - y / (CANVAS_RES - 1);\n        for (let x = 0; x < CANVAS_RES; x++) {\n          const C = (x / (CANVAS_RES - 1)) * CHROMA_MAX;\n          const rgb = oklchToRgb(L, C, hue);\n          const idx = (y * CANVAS_RES + x) * 4;\n          if (!rgb) {\n            img.data[idx] = img.data[idx + 1] = img.data[idx + 2] = 128;\n            img.data[idx + 3] = 255;\n            continue;\n          }\n          img.data[idx] = Math.round(clamp01(rgb.r) * 255);\n          img.data[idx + 1] = Math.round(clamp01(rgb.g) * 255);\n          img.data[idx + 2] = Math.round(clamp01(rgb.b) * 255);\n          img.data[idx + 3] = 255;\n        }\n      }\n      ctx.putImageData(img, 0, 0);\n    });\n    return () => {\n      cancelled = true;\n      cancelAnimationFrame(handle);\n    };\n  }, [hue]);\n\n  useEffect(() => {\n    if (!thumbRef.current) return;\n    const x = clamp01(chroma / CHROMA_MAX);\n    const y = clamp01(1 - lightness);\n    thumbRef.current.style.setProperty('--thumb-x', String(x));\n    thumbRef.current.style.setProperty('--thumb-y', String(y));\n    if (!isThumbVisible.current) {\n      isThumbVisible.current = true;\n      thumbRef.current.style.opacity = '1';\n    }\n  }, [lightness, chroma]);\n\n  // Shared color-computation path. Takes normalized 0..1 pad coordinates\n  // (x = chroma axis, y = lightness axis) and writes them into OKLCH state.\n  // Pointer drag and keyboard both go through this so behavior can't diverge.\n  const applyPosition = useCallback(\n    (x: number, y: number) => {\n      setColor({ c: clamp01(x) * CHROMA_MAX, l: 1 - clamp01(y) });\n    },\n    [setColor]\n  );\n\n  const handlePointerMove = useCallback(\n    (event: PointerEvent) => {\n      if (!(isDragging.current && containerRef.current)) return;\n      event.preventDefault();\n      event.stopPropagation();\n      const rect = containerRef.current.getBoundingClientRect();\n      const x = clamp01((event.clientX - rect.left) / rect.width);\n      const y = clamp01((event.clientY - rect.top) / rect.height);\n      applyPosition(x, y);\n    },\n    [applyPosition]\n  );\n\n  const handlePointerUp = useCallback(() => {\n    isDragging.current = false;\n    window.removeEventListener('pointermove', handlePointerMove);\n    window.removeEventListener('pointerup', handlePointerUp);\n    window.removeEventListener('pointercancel', handlePointerUp);\n  }, [handlePointerMove]);\n\n  const handlePointerDown = useCallback(\n    (e: ReactPointerEvent<HTMLDivElement>) => {\n      e.preventDefault();\n      isDragging.current = true;\n      handlePointerMove(e.nativeEvent);\n      window.addEventListener('pointermove', handlePointerMove);\n      window.addEventListener('pointerup', handlePointerUp);\n      // pointercancel fires instead of pointerup when the OS/browser preempts\n      // the gesture (system dialog, palm rejection, etc.). Handling it with the\n      // same cleanup prevents stranded listeners + isDragging stuck at true.\n      window.addEventListener('pointercancel', handlePointerUp);\n    },\n    [handlePointerMove, handlePointerUp]\n  );\n\n  const handleKeyDown = useCallback(\n    (e: ReactKeyboardEvent<HTMLDivElement>) => {\n      // Current thumb position, mirrored from the same math the thumb effect\n      // uses (x = chroma / CHROMA_MAX, y = 1 - lightness).\n      let x = clamp01(chroma / CHROMA_MAX);\n      let y = clamp01(1 - lightness);\n      const step = e.shiftKey ? STEP_LARGE : STEP;\n      switch (e.key) {\n        case 'ArrowLeft':\n          x -= step;\n          break;\n        case 'ArrowRight':\n          x += step;\n          break;\n        case 'ArrowUp':\n          y -= step; // up = more lightness (y = 1 - L)\n          break;\n        case 'ArrowDown':\n          y += step;\n          break;\n        case 'PageUp':\n          y -= STEP_LARGE;\n          break;\n        case 'PageDown':\n          y += STEP_LARGE;\n          break;\n        case 'Home':\n          x = 0; // no chroma\n          break;\n        case 'End':\n          x = 1; // max chroma\n          break;\n        default:\n          return; // let other keys (Tab, etc.) pass through\n      }\n      e.preventDefault();\n      applyPosition(x, y);\n    },\n    [applyPosition, chroma, lightness]\n  );\n\n  const valueText =\n    `chroma ${Math.round((chroma / CHROMA_MAX) * 100)}%, ` +\n    `lightness ${Math.round(lightness * 100)}%`;\n\n  return (\n    <div\n      className={cx(styles.selectionRoot, className)}\n      onPointerDown={handlePointerDown}\n      onKeyDown={handleKeyDown}\n      ref={containerRef}\n      role='slider'\n      tabIndex={0}\n      aria-label='Color area, chroma and lightness'\n      aria-valuetext={valueText}\n      aria-valuemin={0}\n      aria-valuemax={100}\n      aria-valuenow={Math.round((chroma / CHROMA_MAX) * 100)}\n      data-slot='color-picker-area'\n      {...props}\n    >\n      <canvas\n        ref={canvasRef}\n        width={CANVAS_RES}\n        height={CANVAS_RES}\n        className={styles.selectionCanvas}\n        data-slot='color-picker-area-canvas'\n      />\n      <div\n        className={cx(styles.sliderThumb, styles.selectionThumb)}\n        ref={thumbRef}\n        style={{ background: thumbColor, opacity: 0 }}\n        data-slot='color-picker-area-thumb'\n      />\n    </div>\n  );\n};\n\n// Non-OKLCH modes: classic HSL saturation × scaled-lightness square (pre-OKLCH\n// behavior). State is still stored as OKLCH; we derive HSL for display and\n// convert back on edit so the rest of the picker keeps a single source of\n// truth.\nconst HslArea = ({ className, ...props }: ColorPickerAreaProps) => {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const thumbRef = useRef<HTMLDivElement>(null);\n  const isDragging = useRef(false);\n  const isThumbVisible = useRef(false);\n\n  const { lightness, chroma, hue, setColor } = useColorPicker();\n  const hsl = useMemo(\n    () => oklchToHsl({ l: lightness, c: chroma, h: hue }),\n    [lightness, chroma, hue]\n  );\n\n  const background = useMemo(\n    () =>\n      `linear-gradient(0deg, rgba(0,0,0,1), rgba(0,0,0,0)),\n       linear-gradient(90deg, rgba(255,255,255,1), rgba(255,255,255,0)),\n       hsl(${hsl.h}, 100%, 50%)`,\n    [hsl.h]\n  );\n\n  useEffect(() => {\n    if (!thumbRef.current) return;\n    const x = clamp01(hsl.s / 100);\n    const topLightness = x < 0.01 ? 100 : 50 + 50 * (1 - x);\n    const y = clamp01(1 - hsl.l / topLightness);\n    thumbRef.current.style.setProperty('--thumb-x', String(x));\n    thumbRef.current.style.setProperty('--thumb-y', String(y));\n    if (!isThumbVisible.current) {\n      isThumbVisible.current = true;\n      thumbRef.current.style.opacity = '1';\n    }\n  }, [hsl.s, hsl.l]);\n\n  // Shared color-computation path. Takes normalized 0..1 pad coordinates\n  // (x = saturation axis, y = scaled-lightness axis) and round-trips through\n  // hslToOklch into OKLCH state. Pointer drag and keyboard both go through\n  // this so behavior can't diverge.\n  const applyPosition = useCallback(\n    (x: number, y: number) => {\n      const cx0 = clamp01(x);\n      const saturation = cx0 * 100;\n      const topLightness = cx0 < 0.01 ? 100 : 50 + 50 * (1 - cx0);\n      const nextL = topLightness * (1 - clamp01(y));\n      const next = hslToOklch(hsl.h, saturation, nextL);\n      setColor({ l: next.l, c: next.c, h: next.h });\n    },\n    [hsl.h, setColor]\n  );\n\n  const handlePointerMove = useCallback(\n    (event: PointerEvent) => {\n      if (!(isDragging.current && containerRef.current)) return;\n      event.preventDefault();\n      event.stopPropagation();\n      const rect = containerRef.current.getBoundingClientRect();\n      const x = clamp01((event.clientX - rect.left) / rect.width);\n      const y = clamp01((event.clientY - rect.top) / rect.height);\n      applyPosition(x, y);\n    },\n    [applyPosition]\n  );\n\n  const handlePointerUp = useCallback(() => {\n    isDragging.current = false;\n    window.removeEventListener('pointermove', handlePointerMove);\n    window.removeEventListener('pointerup', handlePointerUp);\n    window.removeEventListener('pointercancel', handlePointerUp);\n  }, [handlePointerMove]);\n\n  const handlePointerDown = useCallback(\n    (e: ReactPointerEvent<HTMLDivElement>) => {\n      e.preventDefault();\n      isDragging.current = true;\n      handlePointerMove(e.nativeEvent);\n      window.addEventListener('pointermove', handlePointerMove);\n      window.addEventListener('pointerup', handlePointerUp);\n      // pointercancel fires instead of pointerup when the OS/browser preempts\n      // the gesture (system dialog, palm rejection, etc.). Handling it with the\n      // same cleanup prevents stranded listeners + isDragging stuck at true.\n      window.addEventListener('pointercancel', handlePointerUp);\n    },\n    [handlePointerMove, handlePointerUp]\n  );\n\n  const handleKeyDown = useCallback(\n    (e: ReactKeyboardEvent<HTMLDivElement>) => {\n      // Current position mirrored from the same math the thumb effect uses.\n      let x = clamp01(hsl.s / 100);\n      const topLightness = x < 0.01 ? 100 : 50 + 50 * (1 - x);\n      let y = clamp01(1 - hsl.l / topLightness);\n      const step = e.shiftKey ? STEP_LARGE : STEP;\n      switch (e.key) {\n        case 'ArrowLeft':\n          x -= step;\n          break;\n        case 'ArrowRight':\n          x += step;\n          break;\n        case 'ArrowUp':\n          y -= step;\n          break;\n        case 'ArrowDown':\n          y += step;\n          break;\n        case 'PageUp':\n          y -= STEP_LARGE;\n          break;\n        case 'PageDown':\n          y += STEP_LARGE;\n          break;\n        case 'Home':\n          x = 0;\n          break;\n        case 'End':\n          x = 1;\n          break;\n        default:\n          return;\n      }\n      e.preventDefault();\n      applyPosition(x, y);\n    },\n    [applyPosition, hsl.s, hsl.l]\n  );\n\n  const topLightnessNow =\n    hsl.s / 100 < 0.01 ? 100 : 50 + 50 * (1 - hsl.s / 100);\n  const valueText =\n    `saturation ${Math.round(hsl.s)}%, ` +\n    `brightness ${Math.round((hsl.l / topLightnessNow) * 100)}%`;\n\n  return (\n    <div\n      className={cx(styles.selectionRoot, className)}\n      onPointerDown={handlePointerDown}\n      onKeyDown={handleKeyDown}\n      ref={containerRef}\n      role='slider'\n      tabIndex={0}\n      aria-label='Color area, saturation and brightness'\n      aria-valuetext={valueText}\n      aria-valuemin={0}\n      aria-valuemax={100}\n      aria-valuenow={Math.round(hsl.s)}\n      style={{ background }}\n      data-slot='color-picker-area'\n      {...props}\n    >\n      <div\n        className={cx(styles.sliderThumb, styles.selectionThumb)}\n        ref={thumbRef}\n        style={{\n          background: `hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)`,\n          opacity: 0\n        }}\n        data-slot='color-picker-area-thumb'\n      />\n    </div>\n  );\n};\n"],"names":[],"mappings":";;;;;;;;;;AAsBA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAIa;AACX;AACA;AACF;AAEA;AAEA;AACA;AACA;AACE;AACA;AACA;AACA;AACA;AAEA;;;;;;;AAWE;AACA;;;AAEA;AACE;;;AAEA;;;AAGA;;AAEE;AACE;;;;;;;;AAQA;;;;;;;AAON;AACA;;;AAGA;AACF;;;;;;AAME;AACA;AACA;AACE;;;AAGJ;;;;;;AAQE;AAIF;;;;;;AAMI;AACA;AACA;AACF;AAIF;AACE;AACA;AACA;AACA;AACF;AAEA;;AAGI;AACA;AACA;AACA;;;;AAIA;AACF;AAIF;;;;;AAMI;AACA;AACE;;;AAGA;;;AAGA;AACE;;AAEF;;;AAGA;;;AAGA;;;AAGA;AACE;;AAEF;AACE;;AAEF;AACE;;;AAGJ;;AAKJ;;;AAmCF;AAEA;AACA;AACA;AACA;AACA;AACE;AACA;AACA;AACA;AAEA;AACA;AAKA;;;;;;;;AAYE;AACA;AACA;AACA;AACE;;;;;;;;;AAWA;AACA;;AAEA;AACA;;;AAMJ;;;;;;AAMI;AACA;AACA;AACF;AAIF;AACE;AACA;AACA;AACA;AACF;AAEA;;AAGI;AACA;AACA;AACA;;;;AAIA;AACF;AAIF;;;;AAKI;AACA;AACA;AACE;;;AAGA;;;AAGA;;;AAGA;;;AAGA;;;AAGA;;;AAGA;;;AAGA;;;AAGA;;;;AAIF;AACF;AAIF;;AAIE;AAEF;AAqBQ;AACA;AACD;AAKT;;"}