{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "color-picker",
  "title": "Color Picker",
  "description": "A blossom-style color picker with concentric petal layers, circular color bar, and arc slider for lightness control.",
  "dependencies": [],
  "files": [
    {
      "path": "components/ui/color-picker-standalone.tsx",
      "type": "registry:ui",
      "content": "'use client'\n\nimport { forwardRef, useEffect, useRef, type HTMLAttributes, type Ref } from 'react'\n\nimport { cn } from '@/lib/utils'\nimport {\n  BlossomColorPicker as LocalBlossomColorPicker,\n  type BlossomColorPickerOptions,\n} from './blossom picker/BlossomColorPicker'\nimport { DEFAULT_COLORS, INNER_COLORS, OUTER_COLORS } from './blossom picker/constants'\nimport { blossomPickerStyles } from './blossom picker/styles'\nimport type {\n  BlossomColorPickerColor,\n  BlossomColorPickerValue,\n  ColorInput,\n  SliderPosition,\n} from './blossom picker/types'\nimport {\n  createColorOutput,\n  getVisualSaturation,\n  hexToHsl,\n  hslToHex,\n  hslToRgb,\n  hslToString,\n  hslaToString,\n  lightnessToSliderValue,\n  organizeColorsIntoLayers,\n  parseColor,\n  rgbaToString,\n  rgbToHsl,\n  sliderValueToLightness,\n} from './blossom picker/utils'\n\nexport type BlossomPickerVariant = 'blossom' | 'blossom-arc'\n\nexport type BlossomPickerValue = BlossomColorPickerValue\nexport type BlossomPickerColor = BlossomColorPickerColor\nexport type { ColorInput, SliderPosition }\n\nexport interface BlossomPickerProps\n  extends\n    Omit<HTMLAttributes<HTMLDivElement>, 'onChange' | 'defaultValue' | 'value'>,\n    BlossomColorPickerOptions {\n  variant?: BlossomPickerVariant\n}\n\nexport type { BlossomColorPickerValue, BlossomColorPickerColor, BlossomColorPickerOptions }\nexport type ColorPickerProps = BlossomPickerProps\n\nfunction propsToOptions(props: BlossomPickerProps): Partial<BlossomColorPickerOptions> {\n  const options: Partial<BlossomColorPickerOptions> = {}\n\n  if (props.value !== undefined) options.value = props.value\n  if (props.defaultValue !== undefined) options.defaultValue = props.defaultValue\n  if (props.colors !== undefined) options.colors = props.colors\n  if (props.onChange !== undefined) options.onChange = props.onChange\n  if (props.onCollapse !== undefined) options.onCollapse = props.onCollapse\n  if (props.disabled !== undefined) options.disabled = props.disabled\n  if (props.openOnHover !== undefined) options.openOnHover = props.openOnHover\n  if (props.initialExpanded !== undefined) {\n    options.initialExpanded = props.initialExpanded\n  }\n  if (props.animationDuration !== undefined) {\n    options.animationDuration = props.animationDuration\n  }\n  if (props.showAlphaSlider !== undefined) {\n    options.showAlphaSlider = props.showAlphaSlider\n  }\n  if (props.coreSize !== undefined) options.coreSize = props.coreSize\n  if (props.petalSize !== undefined) options.petalSize = props.petalSize\n  if (props.showCoreColor !== undefined) {\n    options.showCoreColor = props.showCoreColor\n  }\n  if (props.sliderPosition !== undefined) {\n    options.sliderPosition = props.sliderPosition\n  }\n  if (props.adaptivePositioning !== undefined) {\n    options.adaptivePositioning = props.adaptivePositioning\n  }\n  if (props.circularBarWidth !== undefined) {\n    options.circularBarWidth = props.circularBarWidth\n  }\n  if (props.sliderWidth !== undefined) options.sliderWidth = props.sliderWidth\n  if (props.sliderOffset !== undefined) options.sliderOffset = props.sliderOffset\n  if (props.collapsible !== undefined) options.collapsible = props.collapsible\n\n  return options\n}\n\nfunction syncRef(ref: Ref<HTMLDivElement> | undefined, node: HTMLDivElement | null) {\n  if (!ref) return\n  if (typeof ref === 'function') {\n    ref(node)\n  } else {\n    ref.current = node\n  }\n}\n\nexport const BlossomPicker = forwardRef<HTMLDivElement, BlossomPickerProps>(\n  ({ className, variant = 'blossom-arc', ...props }, ref) => {\n    const blossomProps = {\n      ...props,\n      showAlphaSlider: variant === 'blossom' ? false : (props.showAlphaSlider ?? true),\n    }\n\n    return <BlossomPickerInner ref={ref} className={className} {...blossomProps} />\n  },\n)\n\nBlossomPicker.displayName = 'BlossomPicker'\n\nconst BlossomPickerInner = forwardRef<HTMLDivElement, Omit<BlossomPickerProps, 'variant'>>(\n  ({ className, ...props }, ref) => {\n    const containerRef = useRef<HTMLDivElement>(null)\n    const pickerRef = useRef<LocalBlossomColorPicker | null>(null)\n\n    useEffect(() => {\n      if (!containerRef.current) return\n      pickerRef.current = new LocalBlossomColorPicker(containerRef.current, propsToOptions(props))\n      return () => {\n        pickerRef.current?.destroy()\n        pickerRef.current = null\n      }\n    }, [])\n\n    useEffect(() => {\n      pickerRef.current?.setOptions(propsToOptions(props))\n    }, [\n      props.value,\n      props.defaultValue,\n      props.colors,\n      props.onChange,\n      props.onCollapse,\n      props.disabled,\n      props.openOnHover,\n      props.initialExpanded,\n      props.animationDuration,\n      props.showAlphaSlider,\n      props.coreSize,\n      props.petalSize,\n      props.showCoreColor,\n      props.sliderPosition,\n      props.adaptivePositioning,\n      props.circularBarWidth,\n      props.sliderWidth,\n      props.sliderOffset,\n      props.collapsible,\n    ])\n\n    return (\n      <>\n        <style dangerouslySetInnerHTML={{ __html: blossomPickerStyles }} />\n        <div\n          ref={(node) => {\n            containerRef.current = node\n            syncRef(ref, node)\n          }}\n          data-disabled={props.disabled || undefined}\n          data-state={props.initialExpanded ? 'expanded' : 'collapsed'}\n          className={cn(className)}\n        />\n      </>\n    )\n  },\n)\n\nBlossomPickerInner.displayName = 'BlossomPickerInner'\n\nexport function BlossomPickerPreview() {\n  return (\n    <BlossomPicker\n      variant=\"blossom-arc\"\n      initialExpanded\n      coreSize={48}\n      petalSize={48}\n      circularBarWidth={14}\n      sliderWidth={14}\n      sliderOffset={38}\n    />\n  )\n}\n\nexport const ColorPicker = BlossomPicker\nexport const BlossomColorPicker = BlossomPicker\n\nexport {\n  DEFAULT_COLORS,\n  INNER_COLORS,\n  OUTER_COLORS,\n  createColorOutput,\n  getVisualSaturation,\n  hexToHsl,\n  hslToHex,\n  hslToRgb,\n  hslToString,\n  hslaToString,\n  lightnessToSliderValue,\n  organizeColorsIntoLayers,\n  parseColor,\n  rgbaToString,\n  rgbToHsl,\n  sliderValueToLightness,\n}\n\nexport default BlossomPicker\n"
    },
    {
      "path": "components/ui/blossom picker/types.ts",
      "type": "registry:ui",
      "content": "export interface BlossomColorPickerValue {\n  hue: number\n  saturation: number\n  lightness?: number\n  originalSaturation?: number\n  alpha: number\n  layer: 'inner' | 'outer'\n}\n\nexport interface BlossomColorPickerColor extends BlossomColorPickerValue {\n  hex: string\n  hsl: string\n  hsla: string\n  rgb: string\n  rgba: string\n  r: number\n  g: number\n  b: number\n}\n\nexport type ColorInput =\n  | string\n  | {\n      h: number\n      s: number\n      l: number\n    }\n\nexport type SliderPosition = 'top' | 'bottom' | 'left' | 'right'\n"
    },
    {
      "path": "components/ui/blossom picker/constants.ts",
      "type": "registry:ui",
      "content": "export const OUTER_COLORS = [\n  { h: 47, s: 97, l: 65 }, // #FCD752\n  { h: 37, s: 98, l: 65 }, // #FDBA50\n  { h: 27, s: 95, l: 64 }, // #FA9C4D\n  { h: 14, s: 90, l: 64 }, // #F6774F\n  { h: 0, s: 85, l: 64 }, // #F15656\n  { h: 327, s: 75, l: 62 }, // #E756A6\n  { h: 285, s: 51, l: 59 }, // #B261CC\n  { h: 257, s: 65, l: 64 }, // #8966DF\n  { h: 225, s: 71, l: 65 }, // #6586E5\n  { h: 202, s: 68, l: 65 }, // #69B5E2\n  { h: 151, s: 43, l: 63 }, // #77C9A2\n  { h: 96, s: 49, l: 67 }, // #A4D483\n]\n\nexport const INNER_COLORS = [\n  { h: 50, s: 95, l: 85 }, // #FDF1B6\n  { h: 26, s: 89, l: 89 }, // #FCE0CA\n  { h: 345, s: 77, l: 88 }, // #F8C8D4\n  { h: 283, s: 47, l: 84 }, // #DEC2E9\n  { h: 209, s: 70, l: 87 }, // #C6DEF5\n  { h: 116, s: 42, l: 87 }, // #D2ECD0\n]\n\nexport const DEFAULT_COLORS = [...INNER_COLORS, ...OUTER_COLORS]\n\nexport const BLOOM_EASING =\n  'linear(0, 0.060 3%, 0.200 7%, 0.420 13%, 0.680 20%, 0.900 28%, 1.020 35%, 1.060 45%, 1.025 53%, 0.997 62%, 1.0 68%)'\nexport const HOVER_DELAY = 100\nexport const PETAL_STAGGER = 20\n\nexport const BAR_GAP = 20\nexport const BAR_WIDTH = 12\nexport const SLIDER_OFFSET = 30\nexport const ARC_GRADIENT_STEPS = 15\n"
    },
    {
      "path": "components/ui/blossom picker/dom-helpers.ts",
      "type": "registry:ui",
      "content": "const SVG_NS = 'http://www.w3.org/2000/svg'\n\nexport function createElement<K extends keyof HTMLElementTagNameMap>(\n  tag: K,\n  styles?: Partial<CSSStyleDeclaration>,\n  attrs?: Record<string, string>,\n): HTMLElementTagNameMap[K] {\n  const el = document.createElement(tag)\n  if (styles) setStyles(el, styles)\n  if (attrs) {\n    for (const [key, val] of Object.entries(attrs)) {\n      el.setAttribute(key, val)\n    }\n  }\n  return el\n}\n\nexport function createSVGElement<K extends keyof SVGElementTagNameMap>(\n  tag: K,\n  attrs?: Record<string, string>,\n): SVGElementTagNameMap[K] {\n  const el = document.createElementNS(SVG_NS, tag)\n  if (attrs) {\n    for (const [key, val] of Object.entries(attrs)) {\n      el.setAttribute(key, val)\n    }\n  }\n  return el\n}\n\nexport function setStyles(\n  el: HTMLElement | SVGElement,\n  styles: Partial<CSSStyleDeclaration>,\n): void {\n  for (const [key, val] of Object.entries(styles)) {\n    if (val !== undefined) {\n      ;(el.style as unknown as Record<string, unknown>)[key] = val\n    }\n  }\n}\n\nexport function setAttributes(el: Element, attrs: Record<string, string>): void {\n  for (const [key, val] of Object.entries(attrs)) {\n    el.setAttribute(key, val)\n  }\n}\n"
    },
    {
      "path": "components/ui/blossom picker/utils.ts",
      "type": "registry:ui",
      "content": "import { BlossomColorPickerColor, ColorInput } from './types'\n\nexport function lightnessToSliderValue(l: number): number {\n  const minLightness = 5\n  const maxLightness = 95\n  const clampedL = Math.max(minLightness, Math.min(maxLightness, l))\n  return ((maxLightness - clampedL) / (maxLightness - minLightness)) * 100\n}\n\nexport function sliderValueToLightness(sliderValue: number): number {\n  const minLightness = 5\n  const maxLightness = 95\n  return maxLightness - (sliderValue / 100) * (maxLightness - minLightness)\n}\n\nexport function hexToHsl(hex: string): { h: number; s: number; l: number } {\n  hex = hex.replace(/^#/, '')\n  const r = parseInt(hex.slice(0, 2), 16) / 255\n  const g = parseInt(hex.slice(2, 4), 16) / 255\n  const b = parseInt(hex.slice(4, 6), 16) / 255\n\n  const max = Math.max(r, g, b)\n  const min = Math.min(r, g, b)\n  const l = (max + min) / 2\n\n  if (max === min) {\n    return { h: 0, s: 0, l: Math.round(l * 100) }\n  }\n\n  const d = max - min\n  const s_val = l > 0.5 ? d / (2 - max - min) : d / (max + min)\n\n  let h_val = 0\n  switch (max) {\n    case r:\n      h_val = ((g - b) / d + (g < b ? 6 : 0)) / 6\n      break\n    case g:\n      h_val = ((b - r) / d + 2) / 6\n      break\n    case b:\n      h_val = ((r - g) / d + 4) / 6\n      break\n  }\n\n  return {\n    h: Math.round(h_val * 360),\n    s: Math.round(s_val * 100),\n    l: Math.round(l * 100),\n  }\n}\n\nexport function rgbToHsl(r: number, g: number, b: number): { h: number; s: number; l: number } {\n  r /= 255\n  g /= 255\n  b /= 255\n  const max = Math.max(r, g, b)\n  const min = Math.min(r, g, b)\n  const l = (max + min) / 2\n\n  if (max === min) return { h: 0, s: 0, l: Math.round(l * 100) }\n\n  const d = max - min\n  const s_val = l > 0.5 ? d / (2 - max - min) : d / (max + min)\n  let h_val = 0\n  switch (max) {\n    case r:\n      h_val = ((g - b) / d + (g < b ? 6 : 0)) / 6\n      break\n    case g:\n      h_val = ((b - r) / d + 2) / 6\n      break\n    case b:\n      h_val = ((r - g) / d + 4) / 6\n      break\n  }\n  return {\n    h: Math.round(h_val * 360),\n    s: Math.round(s_val * 100),\n    l: Math.round(l * 100),\n  }\n}\n\nexport function parseColor(input: ColorInput): {\n  h: number\n  s: number\n  l: number\n} {\n  if (typeof input === 'object') return input\n\n  const str = input.trim().toLowerCase()\n\n  if (str.startsWith('#')) return hexToHsl(str)\n\n  const hslMatch = str.match(/^hsla?\\(\\s*([\\d.]+)[\\s,]+([\\d.]+)%?[\\s,]+([\\d.]+)%?/)\n  if (hslMatch) {\n    return {\n      h: Math.round(parseFloat(hslMatch[1])),\n      s: Math.round(parseFloat(hslMatch[2])),\n      l: Math.round(parseFloat(hslMatch[3])),\n    }\n  }\n\n  const rgbMatch = str.match(/^rgba?\\(\\s*([\\d.]+)[\\s,]+([\\d.]+)[\\s,]+([\\d.]+)/)\n  if (rgbMatch) {\n    return rgbToHsl(parseFloat(rgbMatch[1]), parseFloat(rgbMatch[2]), parseFloat(rgbMatch[3]))\n  }\n\n  return { h: 0, s: 0, l: 50 }\n}\n\nexport function hslToHex(h: number, s: number, l: number): string {\n  const sNorm = s / 100\n  const lNorm = l / 100\n  const c = (1 - Math.abs(2 * lNorm - 1)) * sNorm\n  const x = c * (1 - Math.abs(((h / 60) % 2) - 1))\n  const m = lNorm - c / 2\n\n  let r = 0,\n    g = 0,\n    b = 0\n  if (h >= 0 && h < 60) {\n    r = c\n    g = x\n    b = 0\n  } else if (h >= 60 && h < 120) {\n    r = x\n    g = c\n    b = 0\n  } else if (h >= 120 && h < 180) {\n    r = 0\n    g = c\n    b = x\n  } else if (h >= 180 && h < 240) {\n    r = 0\n    g = x\n    b = c\n  } else if (h >= 240 && h < 300) {\n    r = x\n    g = 0\n    b = c\n  } else {\n    r = c\n    g = 0\n    b = x\n  }\n\n  const toHex = (n: number) => {\n    const hex = Math.round((n + m) * 255).toString(16)\n    return hex.length === 1 ? '0' + hex : hex\n  }\n\n  return `#${toHex(r)}${toHex(g)}${toHex(b)}`\n}\n\nexport function getVisualSaturation(sliderValue: number, baseSaturation: number): number {\n  return sliderValue < 10 ? (sliderValue / 10) * baseSaturation : baseSaturation\n}\n\nexport function hslToString(h: number, s: number, l: number): string {\n  return `hsl(${Math.round(h)}, ${Math.round(s)}%, ${Math.round(l)}%)`\n}\n\nexport function hslaToString(h: number, s: number, l: number, a: number): string {\n  return `hsla(${Math.round(h)}, ${Math.round(s)}%, ${Math.round(l)}%, ${(a / 100).toFixed(2)})`\n}\n\nexport function hslToRgb(h: number, s: number, l: number): { r: number; g: number; b: number } {\n  const sNorm = s / 100\n  const lNorm = l / 100\n  const k = (n: number) => (n + h / 30) % 12\n  const a = sNorm * Math.min(lNorm, 1 - lNorm)\n  const f = (n: number) => lNorm - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)))\n\n  return {\n    r: Math.round(255 * f(0)),\n    g: Math.round(255 * f(8)),\n    b: Math.round(255 * f(4)),\n  }\n}\n\nexport function rgbaToString(r: number, g: number, b: number, a: number): string {\n  return `rgba(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)}, ${(a / 100).toFixed(2)})`\n}\n\nexport function createColorOutput(\n  hue: number,\n  sliderValue: number,\n  visualSaturation: number,\n  baseSaturation: number,\n  lightness: number,\n  alpha: number,\n  layer: 'inner' | 'outer',\n): BlossomColorPickerColor {\n  const { r, g, b } = hslToRgb(hue, visualSaturation, lightness)\n  return {\n    hue,\n    saturation: sliderValue,\n    originalSaturation: baseSaturation,\n    lightness,\n    alpha,\n    layer,\n    r,\n    g,\n    b,\n    hex: hslToHex(hue, visualSaturation, lightness),\n    hsl: hslToString(hue, visualSaturation, lightness),\n    hsla: hslaToString(hue, visualSaturation, lightness, alpha),\n    rgb: `rgb(${r}, ${g}, ${b})`,\n    rgba: rgbaToString(r, g, b, alpha),\n  }\n}\n\nexport function organizeColorsIntoLayers(\n  colors: { h: number; s: number; l: number }[],\n): { h: number; s: number; l: number }[][] {\n  if (!colors || colors.length === 0) return []\n\n  const sortedByLightness = colors.toSorted((a, b) => b.l - a.l)\n  const total = sortedByLightness.length\n\n  let layerCounts: number[] = []\n\n  if (total <= 10) {\n    layerCounts = [total]\n  } else if (total <= 24) {\n    const inner = Math.max(4, Math.floor(total * 0.35))\n    layerCounts = [inner, total - inner]\n  } else if (total <= 42) {\n    const inner = Math.max(5, Math.floor(total * 0.15))\n    const middle = Math.floor(total * 0.35)\n    layerCounts = [inner, middle, total - inner - middle]\n  } else {\n    const inner = Math.max(6, Math.floor(total * 0.1))\n    const mid1 = Math.floor(total * 0.2)\n    const mid2 = Math.floor(total * 0.3)\n    layerCounts = [inner, mid1, mid2, total - inner - mid1 - mid2]\n  }\n\n  const layers: { h: number; s: number; l: number }[][] = []\n  let currentIndex = 0\n\n  for (let i = 0; i < layerCounts.length; i++) {\n    const count = layerCounts[i]\n    const itemsForThisLayer = sortedByLightness.slice(currentIndex, currentIndex + count)\n\n    itemsForThisLayer.sort((a, b) => a.h - b.h)\n\n    if (itemsForThisLayer.length > 0) {\n      layers.push(itemsForThisLayer)\n    }\n    currentIndex += count\n  }\n\n  return layers\n}\n"
    },
    {
      "path": "components/ui/blossom picker/layout.ts",
      "type": "registry:ui",
      "content": "export function calculateLayerRadii(\n  layers: { h: number; s: number; l: number }[][],\n  coreSize: number,\n  petalSize: number,\n): number[] {\n  const radii: number[] = []\n  const W = petalSize\n  const Rc = coreSize / 2\n  const Rp = petalSize / 2\n\n  for (let i = 0; i < layers.length; i++) {\n    const N = layers[i].length\n\n    const overlapFactor = N <= 8 ? 0.45 : N <= 12 ? 0.5 : 0.55\n\n    const lateralGaplessR = (N * W * overlapFactor) / (2 * Math.PI)\n\n    let r\n\n    if (i === 0) {\n      const coreOverlap = N <= 5 ? W * 0.35 : W * 0.25\n      const idealCoreR = Rc + Rp - coreOverlap\n      r = Math.max(idealCoreR, lateralGaplessR)\n    } else {\n      const prevR = radii[i - 1]\n      const prevN = layers[i - 1].length\n\n      const circumference = 2 * Math.PI * prevR\n      const coverage = prevN * W\n      const sparsity = coverage / circumference\n\n      let adaptiveStep = W * 0.35\n\n      if (sparsity < 0.85) {\n        adaptiveStep = W * 0.15\n      } else if (sparsity > 1.1) {\n        adaptiveStep = W * 0.45\n      }\n\n      const idealNestleR = prevR + adaptiveStep\n\n      r = Math.max(idealNestleR, lateralGaplessR)\n\n      r = Math.max(r, prevR + W * 0.1)\n    }\n\n    radii.push(r)\n  }\n\n  return radii\n}\n\nexport function calculateLayerRotations(layers: { h: number; s: number; l: number }[][]): number[] {\n  const rotations: number[] = [0]\n\n  for (let i = 1; i < layers.length; i++) {\n    const prevCount = layers[i - 1].length\n    const offset = 360 / prevCount / 2\n    rotations.push(offset)\n  }\n\n  return rotations\n}\n\nexport function calculateBarRadius(\n  layerRadii: number[],\n  petalSize: number,\n  coreSize: number,\n  barGap: number,\n): number {\n  return layerRadii.length > 0\n    ? layerRadii[layerRadii.length - 1] + petalSize / 2 + barGap\n    : coreSize / 2 + barGap\n}\n\nexport function calculateContainerSize(\n  barRadius: number,\n  circularBarWidth: number,\n  showAlphaSlider: boolean,\n  sliderOffset: number,\n  sliderWidth: number,\n): number {\n  const barExtent = barRadius + circularBarWidth / 2\n  const sliderExtent = showAlphaSlider ? barRadius + sliderOffset + sliderWidth / 2 : 0\n  return Math.max(barExtent, sliderExtent) * 2 + 4\n}\n\nexport function getPetalZIndex(\n  index: number,\n  bottomIndex: number,\n  totalPetals: number,\n  layerIdx: number,\n  totalLayers: number,\n  isBottomLeft: boolean = false,\n  isBottomRight: boolean = false,\n): number {\n  const baseZ = (totalLayers - layerIdx) * 100\n  const maxLocalZ = totalPetals + 10\n\n  if (index === bottomIndex) {\n    if (isBottomRight) return baseZ + maxLocalZ\n    if (isBottomLeft) return baseZ\n    return baseZ\n  }\n\n  const steps = (index - bottomIndex + totalPetals) % totalPetals\n\n  return baseZ + steps\n}\n"
    },
    {
      "path": "components/ui/blossom picker/arc-geometry.ts",
      "type": "registry:ui",
      "content": "import type { SliderPosition } from './types'\n\nexport function polarToCartesian(\n  cx: number,\n  cy: number,\n  r: number,\n  angleInDegrees: number,\n): { x: number; y: number } {\n  const angleInRadians = (angleInDegrees * Math.PI) / 180\n  return {\n    x: cx + r * Math.cos(angleInRadians),\n    y: cy + r * Math.sin(angleInRadians),\n  }\n}\n\nexport function describeArc(\n  cx: number,\n  cy: number,\n  r: number,\n  startAng: number,\n  endAng: number,\n): string {\n  const start = polarToCartesian(cx, cy, r, startAng)\n  const end = polarToCartesian(cx, cy, r, endAng)\n  const largeArcFlag = Math.abs(endAng - startAng) > 180 ? '1' : '0'\n  return `M ${start.x} ${start.y} A ${r} ${r} 0 ${largeArcFlag} 1 ${end.x} ${end.y}`\n}\n\nexport function getCenterAngle(position: SliderPosition): number {\n  switch (position) {\n    case 'top':\n      return -90\n    case 'bottom':\n      return 90\n    case 'left':\n      return 180\n    case 'right':\n    default:\n      return 0\n  }\n}\n\nexport function calculateSliderValueFromPoint(\n  dx: number,\n  dy: number,\n  centerAngle: number,\n  halfSweep: number,\n  position: SliderPosition,\n): number {\n  const angle = Math.atan2(dy, dx) * (180 / Math.PI)\n\n  let normalizedAngle = angle - centerAngle\n  while (normalizedAngle > 180) normalizedAngle -= 360\n  while (normalizedAngle < -180) normalizedAngle += 360\n\n  normalizedAngle = Math.max(-halfSweep, Math.min(halfSweep, normalizedAngle))\n\n  let newValue\n  if (position === 'left') {\n    newValue = ((halfSweep - normalizedAngle) / (2 * halfSweep)) * 100\n  } else {\n    newValue = ((normalizedAngle + halfSweep) / (2 * halfSweep)) * 100\n  }\n\n  return Math.round(Math.max(0, Math.min(100, newValue)))\n}\n\nexport function calculateArcGradientColors(\n  hue: number,\n  baseSaturation: number,\n  steps: number,\n  getVisualSaturation: (sliderValue: number, baseSaturation: number) => number,\n  hslToString: (h: number, s: number, l: number) => string,\n): string[] {\n  return Array.from({ length: steps }, (_, i) => {\n    const t = i / (steps - 1)\n    const saturation = getVisualSaturation(t * 100, baseSaturation)\n    const lightness = 95 - t * 90\n    return hslToString(hue, saturation, Math.max(5, lightness))\n  })\n}\n"
    },
    {
      "path": "components/ui/blossom picker/adaptive.ts",
      "type": "registry:ui",
      "content": "import type { SliderPosition } from './types'\n\nexport interface ComputeAdaptivePositionInput {\n  elementRect: { left: number; top: number; width: number; height: number }\n  containerSize: number\n  currentShiftOffset: { x: number; y: number }\n  windowWidth: number\n  windowHeight: number\n  sliderPosition?: SliderPosition\n  adaptivePositioning: boolean\n  circularBarWidth: number\n  sliderOffset: number\n}\n\nexport interface ComputeAdaptivePositionResult {\n  effectivePosition: SliderPosition\n  shiftOffset: { x: number; y: number }\n}\n\nexport function computeAdaptivePosition({\n  elementRect,\n  containerSize,\n  currentShiftOffset,\n  windowWidth,\n  windowHeight,\n  sliderPosition,\n  adaptivePositioning,\n  circularBarWidth,\n  sliderOffset,\n}: ComputeAdaptivePositionInput): ComputeAdaptivePositionResult {\n  const halfSize = containerSize / 2\n  const centerX = elementRect.left + elementRect.width / 2 - currentShiftOffset.x\n  const centerY = elementRect.top + elementRect.height / 2 - currentShiftOffset.y\n\n  let newShiftX = 0\n  let newShiftY = 0\n\n  if (adaptivePositioning) {\n    const padding = 10\n\n    if (centerX + halfSize > windowWidth - padding) {\n      newShiftX = windowWidth - padding - (centerX + halfSize)\n    } else if (centerX - halfSize < padding) {\n      newShiftX = padding - (centerX - halfSize)\n    }\n\n    if (centerY + halfSize > windowHeight - padding) {\n      newShiftY = windowHeight - padding - (centerY + halfSize)\n    } else if (centerY - halfSize < padding) {\n      newShiftY = padding - (centerY - halfSize)\n    }\n  }\n\n  let effectivePosition: SliderPosition = sliderPosition || 'right'\n\n  if (!sliderPosition) {\n    const spaceRight = windowWidth - (centerX + newShiftX + halfSize)\n    const spaceLeft = centerX + newShiftX - halfSize\n    const spaceTop = centerY + newShiftY - halfSize\n    const spaceBottom = windowHeight - (centerY + newShiftY + halfSize)\n\n    const threshold = sliderOffset + circularBarWidth + 20\n\n    if (spaceRight < threshold && spaceLeft > spaceRight) {\n      effectivePosition = 'left'\n    } else if (spaceLeft < threshold && spaceRight > spaceLeft) {\n      effectivePosition = 'right'\n    } else if (spaceBottom < threshold && spaceTop > spaceBottom) {\n      effectivePosition = 'top'\n    } else if (spaceTop < threshold && spaceBottom > spaceTop) {\n      effectivePosition = 'bottom'\n    } else {\n      effectivePosition = 'right'\n    }\n  }\n\n  return {\n    effectivePosition,\n    shiftOffset: { x: newShiftX, y: newShiftY },\n  }\n}\n"
    },
    {
      "path": "components/ui/blossom picker/styles.ts",
      "type": "registry:ui",
      "content": "export const blossomPickerStyles = `\n.bcp-root {\n  position: relative;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.bcp-container {\n  position: absolute;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.bcp-petal {\n  position: absolute;\n  border-radius: 50%;\n  border: none;\n  padding: 0;\n  background: none;\n  cursor: pointer;\n}\n\n.bcp-petal-visible:focus-visible {\n  outline: none;\n  box-shadow: 0 0 0 2.5px rgba(255, 255, 255, 0.95), 0 0 0 4px color-mix(in srgb, var(--color-accent) 80%, transparent);\n}\n\n.bcp-core {\n  position: relative;\n  border-radius: 50%;\n  border: none;\n  padding: 0;\n  cursor: pointer;\n}\n\n.bcp-core:focus-visible {\n  outline: none;\n  box-shadow: 0 0 0 2.5px color-mix(in srgb, var(--color-accent) 90%, transparent), 0 0 0 5px rgba(255, 255, 255, 0.9);\n}\n\n.bcp-core:disabled {\n  opacity: 0.5;\n  cursor: not-allowed;\n}\n\n.bcp-svg {\n  position: absolute;\n  pointer-events: none;\n}\n\n.bcp-slider-track {\n  pointer-events: auto;\n  cursor: pointer;\n  touch-action: none;\n}\n\n.bcp-slider-handle {\n  pointer-events: auto;\n  cursor: grab;\n  touch-action: none;\n}\n\n.bcp-slider-handle:active {\n  cursor: grabbing;\n}\n\n.bcp-bg-wrapper {\n  position: absolute;\n  border-radius: 50%;\n  pointer-events: none;\n}\n\n.bcp-bg-solid {\n  position: absolute;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  border-radius: 50%;\n  pointer-events: none;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .bcp-petal,\n  .bcp-core,\n  .bcp-bg-solid,\n  .bcp-svg,\n  .bcp-slider-handle {\n    transition-duration: 0ms !important;\n    animation-duration: 0ms !important;\n  }\n}\n`\n"
    },
    {
      "path": "components/ui/blossom picker/BlossomColorPicker.ts",
      "type": "registry:ui",
      "content": "import type {\n  BlossomColorPickerValue,\n  BlossomColorPickerColor,\n  ColorInput,\n  SliderPosition,\n} from './types'\nimport { playHoverSound, playClickSound, playTickSound } from '@/lib/sound'\n\nimport { computeAdaptivePosition } from './adaptive'\nimport {\n  DEFAULT_COLORS,\n  BLOOM_EASING,\n  HOVER_DELAY,\n  PETAL_STAGGER,\n  BAR_GAP,\n  BAR_WIDTH,\n  SLIDER_OFFSET,\n} from './constants'\nimport { createElement, setStyles } from './dom-helpers'\nimport {\n  calculateLayerRadii,\n  calculateLayerRotations,\n  calculateBarRadius,\n  calculateContainerSize,\n  getPetalZIndex,\n} from './layout'\nimport { ArcSliderRenderer } from './renderers/ArcSliderRenderer'\nimport { BackgroundRenderer } from './renderers/BackgroundRenderer'\nimport { ColorBarRenderer } from './renderers/ColorBarRenderer'\nimport { CoreButtonRenderer } from './renderers/CoreButtonRenderer'\nimport { PetalRenderer } from './renderers/PetalRenderer'\nimport {\n  lightnessToSliderValue,\n  sliderValueToLightness,\n  hslaToString,\n  createColorOutput,\n  organizeColorsIntoLayers,\n  getVisualSaturation,\n  parseColor,\n} from './utils'\n\nfunction colorsEqual(a: ColorInput[], b: ColorInput[]): boolean {\n  if (a === b) return true\n  if (a.length !== b.length) return false\n  for (let i = 0; i < a.length; i++) {\n    if (a[i] !== b[i]) return false\n  }\n  return true\n}\n\nexport interface BlossomColorPickerOptions {\n  value?: BlossomColorPickerValue\n  defaultValue?: BlossomColorPickerValue\n  colors?: ColorInput[]\n  onChange?: (color: BlossomColorPickerColor) => void\n  onCollapse?: (color: BlossomColorPickerColor) => void\n  disabled?: boolean\n  openOnHover?: boolean\n  initialExpanded?: boolean\n  animationDuration?: number\n  showAlphaSlider?: boolean\n  coreSize?: number\n  petalSize?: number\n  showCoreColor?: boolean\n  sliderPosition?: SliderPosition\n  adaptivePositioning?: boolean\n  circularBarWidth?: number\n  sliderWidth?: number\n  sliderOffset?: number\n  collapsible?: boolean\n}\n\nconst DEFAULT_VALUE: BlossomColorPickerValue = {\n  hue: 330,\n  saturation: 70,\n  alpha: 50,\n  layer: 'outer',\n}\n\nexport class BlossomColorPicker {\n  private container: HTMLElement\n  private rootEl!: HTMLDivElement\n  private containerEl!: HTMLDivElement\n  private mousePos: { x: number; y: number } | null = null\n  private rafId: number | null = null\n\n  private opts: Required<\n    Omit<BlossomColorPickerOptions, 'value' | 'defaultValue' | 'sliderPosition'>\n  > & {\n    sliderPosition?: SliderPosition\n  }\n\n  private internalValue: BlossomColorPickerValue\n  private controlledValue?: BlossomColorPickerValue\n  private isExpanded = false\n  private isHovering = false\n  private hoveredPetal: { layer: number; index: number } | null = null\n  private selectedHue: number | null = null\n  private prevExpanded = false\n  private shiftOffset = { x: 0, y: 0 }\n  private effectivePosition: SliderPosition = 'right'\n\n  private normalizedColors: { h: number; s: number; l: number }[] = []\n  private layers: { h: number; s: number; l: number }[][] = []\n  private allColors: { h: number; s: number; l: number }[] = []\n  private layerPrefixCounts: number[] = []\n  private layerRadii: number[] = []\n  private layerRotations: number[] = []\n  private barRadius = 0\n  private containerSize = 0\n\n  private petalRenderers: PetalRenderer[] = []\n  private colorBarRenderer!: ColorBarRenderer\n  private arcSliderRenderer: ArcSliderRenderer | null = null\n  private coreButtonRenderer!: CoreButtonRenderer\n  private backgroundRenderer!: BackgroundRenderer\n\n  private hoverTimeout: ReturnType<typeof setTimeout> | null = null\n  private closeTimeout: ReturnType<typeof setTimeout> | null = null\n\n  private boundClickOutside: (e: MouseEvent) => void\n  private boundMouseMove: (e: MouseEvent) => void\n  private boundMouseEnter: () => void\n  private boundMouseLeave: () => void\n\n  constructor(container: HTMLElement, options?: Partial<BlossomColorPickerOptions>) {\n    this.container = container\n\n    const defaultValue = options?.defaultValue ?? DEFAULT_VALUE\n\n    this.opts = {\n      colors: options?.colors ?? [],\n      onChange: options?.onChange ?? (() => {}),\n      onCollapse: options?.onCollapse ?? (() => {}),\n      disabled: options?.disabled ?? false,\n      openOnHover: options?.openOnHover ?? false,\n      initialExpanded: options?.initialExpanded ?? false,\n      animationDuration: options?.animationDuration ?? 300,\n      showAlphaSlider: options?.showAlphaSlider ?? true,\n      coreSize: options?.coreSize ?? 32,\n      petalSize: options?.petalSize ?? 32,\n      showCoreColor: options?.showCoreColor ?? true,\n      sliderPosition: options?.sliderPosition,\n      adaptivePositioning: options?.adaptivePositioning ?? true,\n      circularBarWidth: options?.circularBarWidth ?? BAR_WIDTH,\n      sliderWidth: options?.sliderWidth ?? BAR_WIDTH,\n      sliderOffset: options?.sliderOffset ?? SLIDER_OFFSET,\n      collapsible: options?.collapsible ?? true,\n    }\n\n    this.controlledValue = options?.value\n    this.internalValue = options?.value ?? defaultValue\n    this.isExpanded = !this.opts.collapsible || this.opts.initialExpanded\n    this.prevExpanded = this.isExpanded\n    this.effectivePosition = this.opts.sliderPosition || 'right'\n\n    this.boundClickOutside = this.handleClickOutside.bind(this)\n    this.boundMouseMove = this.handleMouseMove.bind(this)\n    this.boundMouseEnter = this.handleMouseEnter.bind(this)\n    this.boundMouseLeave = this.handleMouseLeave.bind(this)\n\n    this.computeLayout()\n    this.render()\n    this.update()\n    this.bindEvents()\n  }\n\n  setValue(value: BlossomColorPickerValue): void {\n    this.controlledValue = value\n    this.internalValue = value\n    this.update()\n  }\n\n  getValue(): BlossomColorPickerColor {\n    const val = this.currentValue\n    const sliderValue = val.saturation\n    const lightness = sliderValueToLightness(sliderValue)\n    const selectedPetal = this.allColors.find((c) => c.h === val.hue)\n    const pBaseSaturation = selectedPetal?.s ?? 70\n    const visualSaturation = getVisualSaturation(sliderValue, pBaseSaturation)\n\n    return createColorOutput(\n      val.hue,\n      sliderValue,\n      visualSaturation,\n      pBaseSaturation,\n      lightness,\n      val.alpha,\n      val.layer,\n    )\n  }\n\n  expand(): void {\n    this.setExpanded(true)\n  }\n\n  collapse(): void {\n    this.setExpanded(false)\n  }\n\n  toggle(): void {\n    this.setExpanded(!this.isExpanded)\n  }\n\n  setOptions(options: Partial<BlossomColorPickerOptions>): void {\n    let needsRerender = false\n\n    if (options.value !== undefined) {\n      this.controlledValue = options.value\n      this.internalValue = options.value\n    }\n\n    if (options.onChange !== undefined) this.opts.onChange = options.onChange\n    if (options.onCollapse !== undefined) this.opts.onCollapse = options.onCollapse\n    if (options.disabled !== undefined) this.opts.disabled = options.disabled\n    if (options.openOnHover !== undefined) this.opts.openOnHover = options.openOnHover\n    if (options.animationDuration !== undefined)\n      this.opts.animationDuration = options.animationDuration\n    if (options.showCoreColor !== undefined) this.opts.showCoreColor = options.showCoreColor\n    if (options.sliderPosition !== undefined) this.opts.sliderPosition = options.sliderPosition\n    if (options.adaptivePositioning !== undefined)\n      this.opts.adaptivePositioning = options.adaptivePositioning\n\n    if (options.initialExpanded !== undefined) {\n      this.opts.initialExpanded = options.initialExpanded\n    }\n\n    if (options.colors !== undefined && !colorsEqual(options.colors, this.opts.colors)) {\n      this.opts.colors = options.colors\n      needsRerender = true\n    }\n    if (options.coreSize !== undefined && options.coreSize !== this.opts.coreSize) {\n      this.opts.coreSize = options.coreSize\n      needsRerender = true\n    }\n    if (options.petalSize !== undefined && options.petalSize !== this.opts.petalSize) {\n      this.opts.petalSize = options.petalSize\n      needsRerender = true\n    }\n    if (\n      options.circularBarWidth !== undefined &&\n      options.circularBarWidth !== this.opts.circularBarWidth\n    ) {\n      this.opts.circularBarWidth = options.circularBarWidth\n      needsRerender = true\n    }\n    if (options.sliderWidth !== undefined && options.sliderWidth !== this.opts.sliderWidth) {\n      this.opts.sliderWidth = options.sliderWidth\n      needsRerender = true\n    }\n    if (options.sliderOffset !== undefined && options.sliderOffset !== this.opts.sliderOffset) {\n      this.opts.sliderOffset = options.sliderOffset\n      needsRerender = true\n    }\n    if (\n      options.showAlphaSlider !== undefined &&\n      options.showAlphaSlider !== this.opts.showAlphaSlider\n    ) {\n      this.opts.showAlphaSlider = options.showAlphaSlider\n      needsRerender = true\n    }\n    if (options.collapsible !== undefined && options.collapsible !== this.opts.collapsible) {\n      this.opts.collapsible = options.collapsible\n      if (!this.opts.collapsible) {\n        this.isExpanded = true\n      }\n    }\n\n    if (needsRerender) {\n      this.destroyInner()\n      this.computeLayout()\n      this.render()\n    }\n\n    this.update()\n  }\n\n  destroy(): void {\n    if (this.hoverTimeout) {\n      clearTimeout(this.hoverTimeout)\n      this.hoverTimeout = null\n    }\n    if (this.closeTimeout) {\n      clearTimeout(this.closeTimeout)\n      this.closeTimeout = null\n    }\n    if (this.rafId !== null) {\n      cancelAnimationFrame(this.rafId)\n      this.rafId = null\n    }\n    this.unbindEvents()\n    this.destroyInner()\n    if (this.rootEl && this.rootEl.parentNode) {\n      this.rootEl.remove()\n    }\n  }\n\n  private get currentValue(): BlossomColorPickerValue {\n    return this.controlledValue ?? this.internalValue\n  }\n\n  private get baseSaturation(): number {\n    const val = this.currentValue\n    if (val.originalSaturation !== undefined) {\n      return val.originalSaturation\n    }\n    const selectedColor = this.allColors.find((c) => c.h === val.hue)\n    return selectedColor?.s ?? 70\n  }\n\n  private get coreColor(): string {\n    const val = this.currentValue\n    if (this.isExpanded && !this.opts.showCoreColor) return '#FFFFFF'\n    const lightness = val.lightness ?? sliderValueToLightness(val.saturation)\n    const saturation = val.originalSaturation ?? this.baseSaturation\n    return hslaToString(val.hue, saturation, lightness, 100)\n  }\n\n  private get currentLightness(): number {\n    return this.currentValue.lightness ?? (this.currentValue.layer === 'inner' ? 85 : 65)\n  }\n\n  private computeLayout(): void {\n    const colors = this.opts.colors\n    this.normalizedColors = colors && colors.length > 0 ? colors.map(parseColor) : DEFAULT_COLORS\n\n    this.layers = organizeColorsIntoLayers(this.normalizedColors)\n    this.allColors = this.layers.flat()\n\n    this.layerPrefixCounts = [0]\n    for (let i = 1; i < this.layers.length; i++) {\n      this.layerPrefixCounts.push(this.layerPrefixCounts[i - 1] + this.layers[i - 1].length)\n    }\n\n    this.layerRadii = calculateLayerRadii(this.layers, this.opts.coreSize, this.opts.petalSize)\n    this.layerRotations = calculateLayerRotations(this.layers)\n    this.barRadius = calculateBarRadius(\n      this.layerRadii,\n      this.opts.petalSize,\n      this.opts.coreSize,\n      BAR_GAP,\n    )\n    this.containerSize = calculateContainerSize(\n      this.barRadius,\n      this.opts.circularBarWidth,\n      this.opts.showAlphaSlider,\n      this.opts.sliderOffset,\n      this.opts.sliderWidth,\n    )\n  }\n\n  private render(): void {\n    if (!this.rootEl) {\n      this.rootEl = createElement('div')\n      this.rootEl.className = 'bcp-root'\n      this.rootEl.setAttribute('role', 'group')\n      this.rootEl.setAttribute('aria-label', 'Color picker')\n      setStyles(this.rootEl, {\n        width: `${this.opts.coreSize}px`,\n        height: `${this.opts.coreSize}px`,\n      })\n\n      this.containerEl = createElement('div')\n      this.containerEl.className = 'bcp-container'\n      setStyles(this.containerEl, {\n        left: '50%',\n        top: '50%',\n      })\n\n      this.rootEl.appendChild(this.containerEl)\n      this.container.appendChild(this.rootEl)\n    }\n\n    this.backgroundRenderer = new BackgroundRenderer(\n      this.barRadius + this.opts.circularBarWidth / 2,\n      this.opts.animationDuration,\n    )\n    this.containerEl.appendChild(this.backgroundRenderer.el)\n\n    this.colorBarRenderer = new ColorBarRenderer(\n      this.barRadius,\n      this.opts.circularBarWidth,\n      this.opts.animationDuration,\n    )\n    this.containerEl.appendChild(this.colorBarRenderer.el)\n\n    this.petalRenderers = []\n    for (let layerIdx = 0; layerIdx < this.layers.length; layerIdx++) {\n      const layerColors = this.layers[layerIdx]\n      const radius = this.layerRadii[layerIdx]\n      const rotation = this.layerRotations[layerIdx]\n      const previousItemsCount = this.layerPrefixCounts[layerIdx]\n      const totalPetals = layerColors.length\n      const totalLayers = this.layers.length\n      const baseZ = (totalLayers - layerIdx) * 100\n\n      let bottomIndex = 0\n      let minDiff = Infinity\n      for (let i = 0; i < totalPetals; i++) {\n        const angle = (i / totalPetals) * 360 - 90 + rotation\n        const normalizedAngle = ((angle % 360) + 360) % 360\n        const diff = Math.min(Math.abs(normalizedAngle - 90), 360 - Math.abs(normalizedAngle - 90))\n        if (diff < minDiff) {\n          minDiff = diff\n          bottomIndex = i\n        }\n      }\n\n      for (let index = 0; index < layerColors.length; index++) {\n        const color = layerColors[index]\n        const staggerDelay = previousItemsCount * PETAL_STAGGER + index * PETAL_STAGGER\n\n        if (index === bottomIndex) {\n          const underlayPetal = new PetalRenderer({\n            hue: color.h,\n            saturation: color.s,\n            lightness: color.l,\n            index,\n            totalPetals,\n            petalSize: this.opts.petalSize,\n            radius,\n            animationDuration: this.opts.animationDuration,\n            staggerDelay,\n            zIndex: baseZ - 1,\n            rotationOffset: rotation,\n            alpha: 1,\n            pointerEvents: 'none',\n            hasShadow: false,\n            noRing: true,\n          })\n          this.petalRenderers.push(underlayPetal)\n          this.containerEl.appendChild(underlayPetal.el)\n\n          const leftPetal = new PetalRenderer({\n            hue: color.h,\n            saturation: color.s,\n            lightness: color.l,\n            index,\n            totalPetals,\n            petalSize: this.opts.petalSize,\n            radius,\n            animationDuration: this.opts.animationDuration,\n            staggerDelay,\n            zIndex: getPetalZIndex(\n              index,\n              bottomIndex,\n              totalPetals,\n              layerIdx,\n              totalLayers,\n              true,\n              false,\n            ),\n            rotationOffset: rotation,\n            alpha: 1,\n            clip: 'left',\n            pointerEvents: 'none',\n            hasShadow: false,\n          })\n          this.petalRenderers.push(leftPetal)\n          this.containerEl.appendChild(leftPetal.el)\n\n          const rightPetal = new PetalRenderer({\n            hue: color.h,\n            saturation: color.s,\n            lightness: color.l,\n            index,\n            totalPetals,\n            petalSize: this.opts.petalSize,\n            radius,\n            animationDuration: this.opts.animationDuration,\n            staggerDelay,\n            zIndex: getPetalZIndex(\n              index,\n              bottomIndex,\n              totalPetals,\n              layerIdx,\n              totalLayers,\n              false,\n              true,\n            ),\n            rotationOffset: rotation,\n            alpha: 1,\n            clip: 'right',\n            pointerEvents: 'none',\n            hasShadow: false,\n          })\n          this.petalRenderers.push(rightPetal)\n          this.containerEl.appendChild(rightPetal.el)\n\n          const interactionPetal = new PetalRenderer(\n            {\n              hue: color.h,\n              saturation: color.s,\n              lightness: color.l,\n              index,\n              totalPetals,\n              petalSize: this.opts.petalSize,\n              radius,\n              animationDuration: this.opts.animationDuration,\n              staggerDelay,\n              zIndex: baseZ + totalPetals + 20,\n              rotationOffset: rotation,\n              alpha: 0,\n              pointerEvents: 'auto',\n              hasShadow: false,\n            },\n            () => this.handlePetalClick(color, layerIdx),\n            () => {\n              this.hoveredPetal = { layer: layerIdx, index }\n              underlayPetal.update(this.isExpanded, true, this.mousePos)\n              leftPetal.update(this.isExpanded, true, this.mousePos)\n              rightPetal.update(this.isExpanded, true, this.mousePos)\n            },\n            () => {\n              this.hoveredPetal = null\n              underlayPetal.update(this.isExpanded, false, this.mousePos)\n              leftPetal.update(this.isExpanded, false, this.mousePos)\n              rightPetal.update(this.isExpanded, false, this.mousePos)\n            },\n          )\n          this.petalRenderers.push(interactionPetal)\n          this.containerEl.appendChild(interactionPetal.el)\n        } else {\n          const petal = new PetalRenderer(\n            {\n              hue: color.h,\n              saturation: color.s,\n              lightness: color.l,\n              index,\n              totalPetals,\n              petalSize: this.opts.petalSize,\n              radius,\n              animationDuration: this.opts.animationDuration,\n              staggerDelay,\n              zIndex: getPetalZIndex(index, bottomIndex, totalPetals, layerIdx, totalLayers),\n              rotationOffset: rotation,\n              alpha: 1,\n              pointerEvents: 'auto',\n              hasShadow: false,\n            },\n            () => this.handlePetalClick(color, layerIdx),\n            () => {\n              this.hoveredPetal = { layer: layerIdx, index }\n            },\n            () => {\n              this.hoveredPetal = null\n            },\n          )\n          this.petalRenderers.push(petal)\n          this.containerEl.appendChild(petal.el)\n        }\n      }\n    }\n\n    if (this.opts.showAlphaSlider) {\n      this.arcSliderRenderer = new ArcSliderRenderer(\n        this.barRadius,\n        this.opts.sliderWidth,\n        this.opts.sliderOffset,\n        this.opts.animationDuration,\n        (value) => this.handleSliderChange(value),\n        this.effectivePosition,\n      )\n      this.containerEl.appendChild(this.arcSliderRenderer.el)\n    }\n\n    this.coreButtonRenderer = new CoreButtonRenderer(\n      this.opts.coreSize,\n      this.opts.animationDuration,\n      () => this.handleCoreClick(),\n    )\n    this.containerEl.appendChild(this.coreButtonRenderer.el)\n  }\n\n  private update(): void {\n    const val = this.currentValue\n    const duration = this.opts.animationDuration\n\n    if (this.isExpanded && !this.prevExpanded && this.rootEl) {\n      const rootRect = this.rootEl.getBoundingClientRect()\n      const result = computeAdaptivePosition({\n        elementRect: rootRect,\n        containerSize: this.containerSize,\n        currentShiftOffset: { x: 0, y: 0 },\n        windowWidth: window.innerWidth,\n        windowHeight: window.innerHeight,\n        sliderPosition: this.opts.sliderPosition,\n        adaptivePositioning: this.opts.adaptivePositioning,\n        circularBarWidth: this.opts.circularBarWidth,\n        sliderOffset: this.opts.sliderOffset,\n      })\n      this.shiftOffset = result.shiftOffset\n      this.effectivePosition = result.effectivePosition\n    } else if (!this.isExpanded) {\n      this.shiftOffset = { x: 0, y: 0 }\n      this.effectivePosition = this.opts.sliderPosition || 'right'\n    }\n\n    setStyles(this.containerEl, {\n      width: `${this.isExpanded ? this.containerSize : this.opts.coreSize}px`,\n      height: `${this.isExpanded ? this.containerSize : this.opts.coreSize}px`,\n      transform: `translate(calc(-50% + ${this.shiftOffset.x}px), calc(-50% + ${this.shiftOffset.y}px))`,\n      transition: `width ${duration}ms ${BLOOM_EASING}, height ${duration}ms ${BLOOM_EASING}, transform ${duration}ms ${BLOOM_EASING}`,\n      zIndex: this.isExpanded ? '50' : '0',\n    })\n\n    this.backgroundRenderer.update(val.hue, val.saturation, this.currentLightness, this.isExpanded)\n\n    this.colorBarRenderer.update(\n      val.hue,\n      getVisualSaturation(val.saturation, this.baseSaturation),\n      this.currentLightness,\n      val.alpha,\n      this.isExpanded,\n    )\n\n    for (const petal of this.petalRenderers) {\n      const isPetalSelected = this.selectedHue !== null && petal.hue === this.selectedHue\n      petal.setSelected(isPetalSelected)\n      petal.update(this.isExpanded, undefined, this.mousePos)\n    }\n\n    if (this.arcSliderRenderer) {\n      this.arcSliderRenderer.update(\n        val.saturation,\n        val.hue,\n        this.baseSaturation,\n        this.isExpanded,\n        this.effectivePosition,\n      )\n    }\n\n    this.coreButtonRenderer.update(\n      this.coreColor,\n      this.isExpanded,\n      this.isHovering,\n      this.opts.disabled,\n    )\n\n    if (this.prevExpanded && !this.isExpanded) {\n      this.fireOnCollapse()\n    }\n    this.prevExpanded = this.isExpanded\n  }\n\n  private bindEvents(): void {\n    this.containerEl.addEventListener('mouseenter', this.boundMouseEnter)\n    this.containerEl.addEventListener('mouseleave', this.boundMouseLeave)\n    this.containerEl.addEventListener('mousemove', this.boundMouseMove)\n  }\n\n  private unbindEvents(): void {\n    document.removeEventListener('mousedown', this.boundClickOutside)\n    this.containerEl.removeEventListener('mouseenter', this.boundMouseEnter)\n    this.containerEl.removeEventListener('mouseleave', this.boundMouseLeave)\n    this.containerEl.removeEventListener('mousemove', this.boundMouseMove)\n  }\n\n  private handleMouseMove(e: MouseEvent): void {\n    const rect = this.containerEl.getBoundingClientRect()\n    this.mousePos = {\n      x: e.clientX - (rect.left + rect.width / 2),\n      y: e.clientY - (rect.top + rect.height / 2),\n    }\n\n    if (this.isExpanded && !this.rafId) {\n      this.rafId = requestAnimationFrame(() => {\n        this.updateInteractive()\n        this.rafId = null\n      })\n    }\n  }\n\n  private updateInteractive(): void {\n    const val = this.currentValue\n    for (const petal of this.petalRenderers) {\n      petal.update(this.isExpanded, undefined, this.mousePos)\n    }\n\n    this.backgroundRenderer.update(val.hue, val.saturation, this.currentLightness, this.isExpanded)\n\n    this.colorBarRenderer.update(\n      val.hue,\n      getVisualSaturation(val.saturation, this.baseSaturation),\n      this.currentLightness,\n      val.alpha,\n      this.isExpanded,\n    )\n\n    if (this.arcSliderRenderer) {\n      this.arcSliderRenderer.update(\n        val.saturation,\n        val.hue,\n        this.baseSaturation,\n        this.isExpanded,\n        this.effectivePosition,\n      )\n    }\n\n    this.coreButtonRenderer.update(\n      this.coreColor,\n      this.isExpanded,\n      this.isHovering,\n      this.opts.disabled,\n    )\n  }\n\n  private destroyInner(): void {\n    for (const petal of this.petalRenderers) {\n      petal.destroy()\n    }\n    this.petalRenderers = []\n\n    this.colorBarRenderer?.destroy()\n    this.arcSliderRenderer?.destroy()\n    this.arcSliderRenderer = null\n    this.coreButtonRenderer?.destroy()\n    this.backgroundRenderer?.destroy()\n  }\n\n  private setExpanded(expanded: boolean): void {\n    if (!this.opts.collapsible) {\n      this.isExpanded = true\n    } else {\n      this.isExpanded = expanded\n    }\n\n    if (this.isExpanded) {\n      document.addEventListener('mousedown', this.boundClickOutside)\n    } else {\n      document.removeEventListener('mousedown', this.boundClickOutside)\n    }\n\n    this.update()\n  }\n\n  private handleClickOutside(e: MouseEvent): void {\n    if (!this.opts.collapsible) return\n    if (this.containerEl && !this.containerEl.contains(e.target as Node)) {\n      this.setExpanded(false)\n    }\n  }\n\n  private handleMouseEnter(): void {\n    playHoverSound()\n    if (this.opts.disabled || !this.opts.openOnHover || !this.opts.collapsible) return\n\n    if (this.closeTimeout) {\n      clearTimeout(this.closeTimeout)\n      this.closeTimeout = null\n    }\n\n    this.isHovering = true\n    this.hoverTimeout = setTimeout(() => {\n      this.setExpanded(true)\n    }, HOVER_DELAY)\n  }\n\n  private handleMouseLeave(): void {\n    if (this.hoverTimeout) {\n      clearTimeout(this.hoverTimeout)\n      this.hoverTimeout = null\n    }\n\n    this.isHovering = false\n\n    if (this.opts.openOnHover && this.opts.collapsible) {\n      this.closeTimeout = setTimeout(() => {\n        this.setExpanded(false)\n      }, 200)\n    }\n  }\n\n  private handleCoreClick(): void {\n    if (this.opts.disabled || !this.opts.collapsible) return\n    playClickSound()\n    this.setExpanded(!this.isExpanded)\n  }\n\n  private handlePetalClick(color: { h: number; s: number; l: number }, layerIdx: number): void {\n    playClickSound()\n    const sliderValue = lightnessToSliderValue(color.l)\n    const layerStr: 'inner' | 'outer' = layerIdx === 0 ? 'inner' : 'outer'\n    const visualSaturation = color.s\n\n    this.selectedHue = color.h\n\n    const newValue: BlossomColorPickerValue = {\n      hue: color.h,\n      saturation: sliderValue,\n      lightness: color.l,\n      originalSaturation: color.s,\n      layer: layerStr,\n      alpha: this.currentValue.alpha,\n    }\n\n    if (this.controlledValue === undefined) {\n      this.internalValue = newValue\n    }\n\n    this.opts.onChange(\n      createColorOutput(\n        color.h,\n        sliderValue,\n        visualSaturation,\n        color.s,\n        color.l,\n        this.currentValue.alpha,\n        layerStr,\n      ),\n    )\n\n    this.update()\n  }\n\n  private handleSliderChange(sliderValue: number): void {\n    playTickSound()\n    const lightness = sliderValueToLightness(sliderValue)\n    const visualSaturation = getVisualSaturation(sliderValue, this.baseSaturation)\n\n    this.internalValue = {\n      ...this.currentValue,\n      saturation: sliderValue,\n      lightness,\n      originalSaturation: this.baseSaturation,\n    }\n\n    this.opts.onChange(\n      createColorOutput(\n        this.currentValue.hue,\n        sliderValue,\n        visualSaturation,\n        this.baseSaturation,\n        lightness,\n        this.currentValue.alpha,\n        this.currentValue.layer,\n      ),\n    )\n\n    this.update()\n  }\n\n  private fireOnCollapse(): void {\n    const val = this.currentValue\n    const sliderValue = val.saturation\n    const lightness = sliderValueToLightness(sliderValue)\n    const selectedPetal = this.allColors.find((c) => c.h === val.hue)\n    const pBaseSaturation = selectedPetal?.s ?? 70\n    const visualSaturation = getVisualSaturation(sliderValue, pBaseSaturation)\n\n    this.opts.onCollapse(\n      createColorOutput(\n        val.hue,\n        sliderValue,\n        visualSaturation,\n        pBaseSaturation,\n        lightness,\n        val.alpha,\n        val.layer,\n      ),\n    )\n  }\n}\n"
    },
    {
      "path": "components/ui/blossom picker/renderers/BackgroundRenderer.ts",
      "type": "registry:ui",
      "content": "import { BLOOM_EASING } from '../constants'\nimport { createElement, setStyles } from '../dom-helpers'\nimport { hslaToString } from '../utils'\n\nexport class BackgroundRenderer {\n  public el: HTMLDivElement\n  private solidBg: HTMLDivElement\n  private tintEl: HTMLDivElement\n\n  constructor(\n    private radius: number,\n    private animationDuration: number,\n  ) {\n    const size = radius * 2\n    this.el = createElement('div')\n    this.el.className = 'bcp-bg-wrapper'\n    setStyles(this.el, {\n      position: 'absolute',\n      width: `${size}px`,\n      height: `${size}px`,\n      pointerEvents: 'none',\n      zIndex: '0',\n    })\n\n    this.solidBg = createElement('div')\n    this.solidBg.className = 'bcp-bg-solid'\n    setStyles(this.solidBg, {\n      position: 'absolute',\n      inset: '0',\n      backgroundColor: '#FFFFFF',\n      borderRadius: '50%',\n      transform: 'scale(1)',\n      transition: `transform ${animationDuration}ms ${BLOOM_EASING}, opacity ${animationDuration}ms ${BLOOM_EASING}`,\n    })\n    this.el.appendChild(this.solidBg)\n\n    this.tintEl = createElement('div')\n    setStyles(this.tintEl, {\n      position: 'absolute',\n      inset: '0',\n      borderRadius: '50%',\n      transition: `background-color ${animationDuration}ms ease`,\n    })\n    this.solidBg.appendChild(this.tintEl)\n  }\n\n  update(hue: number, saturation: number, lightness: number, isExpanded: boolean): void {\n    setStyles(this.solidBg, {\n      transform: isExpanded ? 'scale(0.9)' : 'scale(0.98)',\n      opacity: isExpanded ? '1' : '0',\n    })\n\n    setStyles(this.tintEl, {\n      backgroundColor: hslaToString(hue, saturation, lightness, 15),\n    })\n  }\n\n  destroy(): void {\n    this.el.remove()\n  }\n}\n"
    },
    {
      "path": "components/ui/blossom picker/renderers/ColorBarRenderer.ts",
      "type": "registry:ui",
      "content": "import { BLOOM_EASING } from '../constants'\nimport { createSVGElement, setStyles } from '../dom-helpers'\nimport { hslaToString } from '../utils'\n\nexport class ColorBarRenderer {\n  public el: SVGSVGElement\n  private bgCircle: SVGCircleElement\n  private colorCircle: SVGCircleElement\n\n  constructor(\n    private radius: number,\n    private barWidth: number,\n    private animationDuration: number,\n  ) {\n    const size = (radius + barWidth / 2) * 2 + 4\n    this.el = createSVGElement('svg', {\n      width: String(size),\n      height: String(size),\n    })\n    this.el.classList.add('bcp-svg')\n    setStyles(this.el, {\n      left: '50%',\n      top: '50%',\n      marginLeft: `${-size / 2}px`,\n      marginTop: `${-size / 2}px`,\n      zIndex: '5',\n    })\n\n    const cx = String(size / 2)\n    const cy = String(size / 2)\n    const r = String(radius)\n    const sw = String(barWidth)\n\n    this.bgCircle = createSVGElement('circle', {\n      cx,\n      cy,\n      r,\n      fill: 'none',\n      stroke: 'rgba(0,0,0,0.06)',\n      'stroke-width': sw,\n    })\n\n    this.colorCircle = createSVGElement('circle', {\n      cx,\n      cy,\n      r,\n      fill: 'none',\n      'stroke-width': sw,\n    })\n\n    this.el.appendChild(this.bgCircle)\n    this.el.appendChild(this.colorCircle)\n  }\n\n  update(\n    hue: number,\n    saturation: number,\n    lightness: number,\n    _alpha: number,\n    isExpanded: boolean,\n  ): void {\n    const color = hslaToString(hue, saturation, lightness, 100)\n    this.colorCircle.setAttribute('stroke', color)\n\n    setStyles(this.el, {\n      opacity: isExpanded ? '1' : '0',\n      transform: isExpanded ? 'scale(1)' : 'scale(0.8)',\n      transition: `opacity ${this.animationDuration}ms ${BLOOM_EASING}, transform ${this.animationDuration}ms ${BLOOM_EASING}`,\n    })\n  }\n\n  destroy(): void {\n    this.el.remove()\n  }\n}\n"
    },
    {
      "path": "components/ui/blossom picker/renderers/CoreButtonRenderer.ts",
      "type": "registry:ui",
      "content": "import { BLOOM_EASING } from '../constants'\nimport { createElement, setStyles } from '../dom-helpers'\nimport { playHoverSound, playClickSound } from '@/lib/sound'\n\nexport class CoreButtonRenderer {\n  public el: HTMLButtonElement\n\n  constructor(\n    private coreSize: number,\n    private animationDuration: number,\n    private onClick: () => void,\n  ) {\n    this.el = createElement('button', undefined, {\n      type: 'button',\n      tabIndex: '0',\n    })\n    this.el.className = 'bcp-core'\n    this.el.addEventListener('mouseenter', () => playHoverSound())\n    this.el.addEventListener('click', () => {\n      playClickSound()\n      this.onClick()\n    })\n\n    setStyles(this.el, {\n      width: `${coreSize}px`,\n      height: `${coreSize}px`,\n      zIndex: '1000',\n    })\n  }\n\n  update(coreColor: string, isExpanded: boolean, isHovering: boolean, disabled: boolean): void {\n    this.el.disabled = disabled\n    this.el.setAttribute('aria-label', `Color picker${isExpanded ? ', expanded' : ''}`)\n    this.el.setAttribute('aria-expanded', String(isExpanded))\n\n    setStyles(this.el, {\n      backgroundColor: coreColor,\n      transform: isExpanded ? 'scale(1)' : isHovering ? 'scale(1.08)' : 'scale(1)',\n      boxShadow: isExpanded\n        ? '0 0 0 2px rgba(0,0,0,0.1), 0 4px 12px rgba(0,0,0,0.15)'\n        : isHovering\n          ? '0 4px 16px rgba(0,0,0,0.2)'\n          : '0 2px 8px rgba(0,0,0,0.15)',\n      transition: `transform 150ms cubic-bezier(0.22, 1, 0.36, 1), box-shadow 150ms ease`,\n    })\n  }\n\n  destroy(): void {\n    this.el.remove()\n  }\n}\n"
    },
    {
      "path": "components/ui/blossom picker/renderers/PetalRenderer.ts",
      "type": "registry:ui",
      "content": "import { BLOOM_EASING } from '../constants'\nimport { createElement, setStyles } from '../dom-helpers'\nimport { hslToString, hslaToString } from '../utils'\nimport { playHoverSound, playClickSound } from '@/lib/sound'\n\nexport interface PetalConfig {\n  hue: number\n  saturation: number\n  lightness: number\n  index: number\n  totalPetals: number\n  petalSize: number\n  radius: number\n  animationDuration: number\n  staggerDelay: number\n  zIndex: number\n  rotationOffset: number\n  alpha: number\n  clip?: 'left' | 'right'\n  pointerEvents: 'auto' | 'none'\n  hasShadow: boolean\n  noRing?: boolean\n}\n\nexport class PetalRenderer {\n  public el: HTMLButtonElement\n  private config: PetalConfig\n  private isHovered = false\n  private isSelected = false\n  private interactive: boolean\n\n  get hue(): number {\n    return this.config.hue\n  }\n\n  constructor(\n    config: PetalConfig,\n    private onClick?: () => void,\n    private onMouseEnter?: () => void,\n    private onMouseLeave?: () => void,\n  ) {\n    this.config = config\n    this.interactive = !!onClick\n    this.el = createElement(\n      'button',\n      undefined,\n      this.interactive\n        ? {\n            type: 'button',\n            'aria-label': `Select color hue ${config.hue}`,\n            tabIndex: '-1',\n          }\n        : {\n            type: 'button',\n            'aria-hidden': 'true',\n            tabIndex: '-1',\n          },\n    )\n    this.el.className = 'bcp-petal'\n\n    if (config.alpha !== 0) {\n      this.el.classList.add('bcp-petal-visible')\n    }\n\n    this.el.addEventListener('click', () => {\n      playClickSound()\n      this.onClick?.()\n    })\n    this.el.addEventListener('mouseenter', () => {\n      this.isHovered = true\n      playHoverSound()\n      this.onMouseEnter?.()\n      this.updateStyles(this.lastExpanded)\n    })\n    this.el.addEventListener('mouseleave', () => {\n      this.isHovered = false\n      this.onMouseLeave?.()\n      this.updateStyles(this.lastExpanded)\n    })\n\n    this.applyBaseStyles()\n    this.updateStyles(false)\n  }\n\n  private lastExpanded = false\n\n  private applyBaseStyles(): void {\n    const { petalSize, config: c } = {\n      petalSize: this.config.petalSize,\n      config: this.config,\n    }\n    setStyles(this.el, {\n      position: 'absolute',\n      width: `${petalSize}px`,\n      height: `${petalSize}px`,\n      borderRadius: '50%',\n      border: 'none',\n      padding: '0',\n      background: 'none',\n      left: '50%',\n      top: '50%',\n      marginLeft: `${-petalSize / 2}px`,\n      marginTop: `${-petalSize / 2}px`,\n    })\n\n    if (c.clip === 'left') {\n      this.el.style.clipPath = 'polygon(0% -50%, 50% -50%, 50% 150%, 0% 150%)'\n    } else if (c.clip === 'right') {\n      this.el.style.clipPath = 'polygon(50% -50%, 100% -50%, 100% 150%, 50% 150%)'\n    }\n  }\n\n  setSelected(selected: boolean): void {\n    this.isSelected = selected\n    this.updateStyles(this.lastExpanded)\n  }\n\n  update(\n    isExpanded: boolean,\n    externalHover?: boolean,\n    mousePos?: { x: number; y: number } | null,\n  ): void {\n    if (externalHover !== undefined) {\n      this.isHovered = externalHover\n    }\n    this.updateStyles(isExpanded, mousePos)\n  }\n\n  private updateStyles(isExpanded: boolean, mousePos?: { x: number; y: number } | null): void {\n    const isExpanding = isExpanded && !this.lastExpanded\n    const c = this.config\n    const isHovered = this.isHovered\n    const isInvisible = c.alpha === 0\n\n    const angle = (c.index / c.totalPetals) * 360 - 90 + c.rotationOffset\n    const radian = (angle * Math.PI) / 180\n    let x = Math.cos(radian) * c.radius\n    let y = Math.sin(radian) * c.radius\n\n    if (isExpanded && mousePos && !isHovered && !isInvisible) {\n      const dx = x - mousePos.x\n      const dy = y - mousePos.y\n      const dist = Math.sqrt(dx * dx + dy * dy)\n      const minDistance = 60\n\n      if (dist < minDistance) {\n        const pushStrength = (1 - dist / minDistance) * 6\n        const pushAngle = Math.atan2(dy, dx)\n        x += Math.cos(pushAngle) * pushStrength\n        y += Math.sin(pushAngle) * pushStrength\n      }\n    }\n\n    const color =\n      c.alpha < 1\n        ? hslaToString(c.hue, c.saturation, c.lightness, c.alpha * 100)\n        : hslToString(c.hue, c.saturation, c.lightness)\n\n    const scale = isHovered ? 1.12 : this.isSelected ? 1.05 : 1\n\n    const transformTransition =\n      isExpanded && !isExpanding && mousePos && !isHovered\n        ? 'transform 150ms cubic-bezier(0.22, 1, 0.36, 1)'\n        : `transform ${c.animationDuration}ms ${BLOOM_EASING} ${isExpanded && !isHovered ? c.staggerDelay : 0}ms`\n\n    setStyles(this.el, {\n      backgroundColor: color,\n      transform: isExpanded\n        ? `translate(${x}px, ${y}px) scale(${scale})`\n        : 'translate(0, 0) scale(0.85)',\n      opacity: isExpanded ? '1' : '0',\n      filter: isHovered && !isInvisible ? 'brightness(1.15) saturate(1.1)' : 'brightness(1)',\n      transition: `${transformTransition},\n                   opacity ${c.animationDuration}ms ${BLOOM_EASING} ${isExpanded && !isHovered ? c.staggerDelay : 0}ms,\n                   background-color 150ms ease,\n                   box-shadow 150ms ease,\n                   filter 150ms ease`,\n      boxShadow:\n        c.hasShadow && !isInvisible\n          ? isHovered\n            ? '0 6px 16px rgba(0,0,0,0.3)'\n            : this.isSelected\n              ? '0 0 0 2.5px rgba(255,255,255,0.95), 0 4px 12px rgba(0,0,0,0.2)'\n              : '0 2px 6px rgba(0,0,0,0.15)'\n          : 'none',\n      zIndex: String(c.zIndex),\n      pointerEvents: c.pointerEvents,\n    })\n\n    if (this.interactive) {\n      this.el.tabIndex = isExpanded ? 0 : -1\n    }\n    this.lastExpanded = isExpanded\n  }\n\n  destroy(): void {\n    this.el.remove()\n  }\n}\n"
    },
    {
      "path": "components/ui/blossom picker/renderers/ArcSliderRenderer.ts",
      "type": "registry:ui",
      "content": "import {\n  polarToCartesian,\n  describeArc,\n  getCenterAngle,\n  calculateSliderValueFromPoint,\n  calculateArcGradientColors,\n} from '../arc-geometry'\nimport { BLOOM_EASING, ARC_GRADIENT_STEPS } from '../constants'\nimport { createSVGElement, setStyles, setAttributes } from '../dom-helpers'\nimport type { SliderPosition } from '../types'\nimport { sliderValueToLightness, hslToString, getVisualSaturation } from '../utils'\nimport { playHoverSound, playClickSound } from '@/lib/sound'\n\nlet arcIdCounter = 0\n\nexport class ArcSliderRenderer {\n  public el: SVGSVGElement\n  private bgPath: SVGPathElement\n  private gradientPath: SVGPathElement\n  private handle: SVGCircleElement\n  private gradient: SVGLinearGradientElement\n  private gradientStops: SVGStopElement[] = []\n\n  private isDragging = false\n  private svgSize: number\n  private center: number\n  private arcRadius: number\n  private halfSweep = 30\n  private handleRadius: number\n  private gradientId: string\n\n  private currentPosition: SliderPosition\n  private currentValue = 50\n  private currentHue = 0\n  private currentBaseSaturation = 70\n  private animationDuration: number\n\n  private boundMouseMove: (e: MouseEvent) => void\n  private boundTouchMove: (e: TouchEvent) => void\n  private boundEnd: () => void\n  private boundKeyDown: (e: KeyboardEvent) => void\n\n  constructor(\n    barRadius: number,\n    private barWidth: number,\n    sliderOffset: number,\n    animationDuration: number,\n    private onChange: (value: number) => void,\n    position: SliderPosition = 'right',\n  ) {\n    this.animationDuration = animationDuration\n    this.currentPosition = position\n    this.arcRadius = barRadius + sliderOffset\n    this.handleRadius = barWidth / 2\n    this.svgSize = (this.arcRadius + this.handleRadius + this.barWidth) * 2 + 20\n    this.center = this.svgSize / 2\n\n    this.gradientId = `bcp-arc-grad-${++arcIdCounter}`\n\n    this.el = createSVGElement('svg', {\n      width: String(this.svgSize),\n      height: String(this.svgSize),\n    })\n    this.el.classList.add('bcp-svg')\n    setStyles(this.el, {\n      left: '50%',\n      top: '50%',\n      marginLeft: `${-this.svgSize / 2}px`,\n      marginTop: `${-this.svgSize / 2}px`,\n      zIndex: '50',\n    })\n\n    const defs = createSVGElement('defs')\n    this.gradient = createSVGElement('linearGradient', {\n      id: this.gradientId,\n      gradientUnits: 'userSpaceOnUse',\n    })\n\n    for (let i = 0; i < ARC_GRADIENT_STEPS; i++) {\n      const stop = createSVGElement('stop', {\n        offset: `${(i / (ARC_GRADIENT_STEPS - 1)) * 100}%`,\n        'stop-color': '#fff',\n      })\n      this.gradientStops.push(stop)\n      this.gradient.appendChild(stop)\n    }\n\n    defs.appendChild(this.gradient)\n    this.el.appendChild(defs)\n\n    this.bgPath = createSVGElement('path', {\n      fill: 'none',\n      stroke: 'rgba(0,0,0,0.06)',\n      'stroke-width': String(this.barWidth),\n      'stroke-linecap': 'round',\n    })\n    this.el.appendChild(this.bgPath)\n\n    this.gradientPath = createSVGElement('path', {\n      fill: 'none',\n      stroke: `url(#${this.gradientId})`,\n      'stroke-width': String(this.barWidth),\n      'stroke-linecap': 'round',\n    })\n    this.gradientPath.classList.add('bcp-slider-track')\n    this.gradientPath.addEventListener('click', (e) => {\n      this.handleTrackClick(e)\n    })\n    this.el.appendChild(this.gradientPath)\n\n    this.handle = createSVGElement('circle', {\n      r: String(this.handleRadius),\n      fill: '#fff',\n      stroke: 'white',\n      'stroke-width': '2',\n      tabindex: '0',\n      role: 'slider',\n      'aria-label': 'Lightness',\n      'aria-valuemin': '0',\n      'aria-valuemax': '100',\n      'aria-valuenow': String(this.currentValue),\n    })\n    this.handle.classList.add('bcp-slider-handle')\n    this.handle.addEventListener('mouseenter', () => playHoverSound())\n    this.handle.addEventListener('mousedown', (e) => {\n      e.preventDefault()\n      playClickSound()\n      this.startDrag()\n    })\n    this.handle.addEventListener('touchstart', (e) => {\n      e.preventDefault()\n      playClickSound()\n      this.startDrag()\n    })\n    this.el.appendChild(this.handle)\n\n    this.boundMouseMove = (e: MouseEvent) => this.calculateValueFromEvent(e)\n    this.boundTouchMove = (e: TouchEvent) => this.calculateValueFromEvent(e)\n    this.boundEnd = () => this.endDrag()\n    this.boundKeyDown = (e: KeyboardEvent) => this.handleKeyDown(e)\n    this.handle.addEventListener('keydown', this.boundKeyDown)\n\n    this.updateGeometry()\n  }\n\n  private startDrag(): void {\n    this.isDragging = true\n    window.addEventListener('mousemove', this.boundMouseMove)\n    window.addEventListener('mouseup', this.boundEnd)\n    window.addEventListener('touchmove', this.boundTouchMove, {\n      passive: false,\n    })\n    window.addEventListener('touchend', this.boundEnd)\n  }\n\n  private endDrag(): void {\n    this.isDragging = false\n    window.removeEventListener('mousemove', this.boundMouseMove)\n    window.removeEventListener('mouseup', this.boundEnd)\n    window.removeEventListener('touchmove', this.boundTouchMove)\n    window.removeEventListener('touchend', this.boundEnd)\n    this.updateHandleTransition()\n  }\n\n  private handleTrackClick(e: MouseEvent): void {\n    this.calculateValueFromEvent(e)\n  }\n\n  private handleKeyDown(e: KeyboardEvent): void {\n    const step = e.shiftKey ? 10 : 1\n    let next: number | null = null\n\n    if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {\n      next = Math.min(100, this.currentValue + step)\n    } else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {\n      next = Math.max(0, this.currentValue - step)\n    } else if (e.key === 'Home') {\n      next = 0\n    } else if (e.key === 'End') {\n      next = 100\n    } else if (e.key === 'PageUp') {\n      next = Math.min(100, this.currentValue + 10)\n    } else if (e.key === 'PageDown') {\n      next = Math.max(0, this.currentValue - 10)\n    }\n\n    if (next !== null) {\n      e.preventDefault()\n      this.onChange(next)\n    }\n  }\n\n  private calculateValueFromEvent(e: MouseEvent | TouchEvent): void {\n    if ('touches' in e) {\n      e.preventDefault()\n    }\n    const rect = this.el.getBoundingClientRect()\n    const centerX = rect.left + rect.width / 2\n    const centerY = rect.top + rect.height / 2\n\n    const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX\n    const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY\n\n    const dx = clientX - centerX\n    const dy = clientY - centerY\n    const centerAngle = getCenterAngle(this.currentPosition)\n    const value = calculateSliderValueFromPoint(\n      dx,\n      dy,\n      centerAngle,\n      this.halfSweep,\n      this.currentPosition,\n    )\n\n    this.onChange(value)\n  }\n\n  private updateGeometry(): void {\n    const centerAngle = getCenterAngle(this.currentPosition)\n    const drawStartAngle = centerAngle - this.halfSweep\n    const drawEndAngle = centerAngle + this.halfSweep\n\n    const arcD = describeArc(this.center, this.center, this.arcRadius, drawStartAngle, drawEndAngle)\n    this.bgPath.setAttribute('d', arcD)\n    this.gradientPath.setAttribute('d', arcD)\n\n    const valStartAngle = this.currentPosition === 'left' ? drawEndAngle : drawStartAngle\n    const valEndAngle = this.currentPosition === 'left' ? drawStartAngle : drawEndAngle\n\n    const gradStart = polarToCartesian(this.center, this.center, this.arcRadius, valStartAngle)\n    const gradEnd = polarToCartesian(this.center, this.center, this.arcRadius, valEndAngle)\n\n    setAttributes(this.gradient, {\n      x1: String(gradStart.x),\n      y1: String(gradStart.y),\n      x2: String(gradEnd.x),\n      y2: String(gradEnd.y),\n    })\n  }\n\n  update(\n    value: number,\n    hue: number,\n    baseSaturation: number,\n    isExpanded: boolean,\n    position: SliderPosition,\n  ): void {\n    this.currentValue = value\n    this.currentHue = hue\n    this.currentBaseSaturation = baseSaturation\n\n    if (position !== this.currentPosition) {\n      this.currentPosition = position\n      this.updateGeometry()\n    }\n\n    const gradientColors = calculateArcGradientColors(\n      hue,\n      baseSaturation,\n      ARC_GRADIENT_STEPS,\n      getVisualSaturation,\n      hslToString,\n    )\n    for (let i = 0; i < this.gradientStops.length; i++) {\n      this.gradientStops[i].setAttribute('stop-color', gradientColors[i])\n    }\n\n    const centerAngle = getCenterAngle(position)\n    const drawStartAngle = centerAngle - this.halfSweep\n    const drawEndAngle = centerAngle + this.halfSweep\n    const valStartAngle = position === 'left' ? drawEndAngle : drawStartAngle\n    const valEndAngle = position === 'left' ? drawStartAngle : drawEndAngle\n\n    const handleAngle = valStartAngle + (value / 100) * (valEndAngle - valStartAngle)\n    const handlePos = polarToCartesian(this.center, this.center, this.arcRadius, handleAngle)\n\n    const handleLightness = sliderValueToLightness(value)\n    const handleSaturation = getVisualSaturation(value, baseSaturation)\n    const handleColor = hslToString(hue, handleSaturation, handleLightness)\n\n    this.handle.setAttribute('cx', String(handlePos.x))\n    this.handle.setAttribute('cy', String(handlePos.y))\n    this.handle.setAttribute('fill', handleColor)\n    this.handle.setAttribute('aria-valuenow', String(Math.round(value)))\n\n    this.updateHandleTransition()\n\n    setStyles(this.el, {\n      opacity: isExpanded ? '1' : '0',\n      transform: isExpanded ? 'scale(1)' : 'scale(0.8)',\n      transition: `opacity ${this.animationDuration}ms ${BLOOM_EASING} ${this.animationDuration / 2}ms, transform ${this.animationDuration}ms ${BLOOM_EASING} ${this.animationDuration / 2}ms`,\n    })\n  }\n\n  private updateHandleTransition(): void {\n    setStyles(this.handle, {\n      transition: this.isDragging\n        ? 'none'\n        : `cx ${this.animationDuration / 3}ms ease, cy ${this.animationDuration / 3}ms ease`,\n    })\n  }\n\n  destroy(): void {\n    window.removeEventListener('mousemove', this.boundMouseMove)\n    window.removeEventListener('mouseup', this.boundEnd)\n    window.removeEventListener('touchmove', this.boundTouchMove)\n    window.removeEventListener('touchend', this.boundEnd)\n    this.handle.removeEventListener('keydown', this.boundKeyDown)\n    this.el.remove()\n  }\n}\n"
    },
    {
      "path": "lib/sound.ts",
      "type": "registry:lib",
      "content": "'use client'\n\nconst MUTE_KEY = 'sound-muted'\nconst MUTE_EVENT = 'sound-mute-change'\n\nlet muted = false\nif (typeof window !== 'undefined') {\n  try {\n    muted = localStorage.getItem(MUTE_KEY) === '1'\n  } catch {}\n}\n\nexport function isSoundMuted(): boolean {\n  return muted\n}\n\nexport function setSoundMuted(value: boolean) {\n  muted = value\n  if (typeof window !== 'undefined') {\n    try {\n      localStorage.setItem(MUTE_KEY, value ? '1' : '0')\n    } catch {}\n    window.dispatchEvent(new CustomEvent(MUTE_EVENT))\n  }\n}\n\nexport function toggleSoundMuted(): boolean {\n  setSoundMuted(!muted)\n  return muted\n}\n\nexport function subscribeSoundMuted(callback: () => void) {\n  if (typeof window === 'undefined') return () => {}\n  window.addEventListener(MUTE_EVENT, callback)\n  const onStorage = (e: StorageEvent) => {\n    if (e.key === MUTE_KEY) callback()\n  }\n  window.addEventListener('storage', onStorage)\n  return () => {\n    window.removeEventListener(MUTE_EVENT, callback)\n    window.removeEventListener('storage', onStorage)\n  }\n}\n\nlet audioCtx: AudioContext | null = null\n\nfunction initAudio(): AudioContext | null {\n  if (typeof window === 'undefined') return null\n  if (!audioCtx) {\n    const AudioContextClass =\n      window.AudioContext ||\n      (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext\n    if (AudioContextClass) {\n      audioCtx = new AudioContextClass()\n    }\n  }\n  if (audioCtx && audioCtx.state === 'suspended') {\n    audioCtx.resume().catch(() => {})\n  }\n  return audioCtx\n}\n\nif (typeof window !== 'undefined') {\n  const unlock = () => {\n    initAudio()\n    window.removeEventListener('pointerdown', unlock)\n    window.removeEventListener('keydown', unlock)\n    window.removeEventListener('touchstart', unlock)\n  }\n  window.addEventListener('pointerdown', unlock, { passive: true })\n  window.addEventListener('keydown', unlock, { passive: true })\n  window.addEventListener('touchstart', unlock, { passive: true })\n}\n\nexport function getAudioContext(): AudioContext | null {\n  return initAudio()\n}\n\nexport function playHoverSound(volume = 0.12, pitch = 1.2) {\n  if (muted) return\n  try {\n    const ctx = getAudioContext()\n    if (!ctx) return\n    const now = ctx.currentTime\n\n    const osc = ctx.createOscillator()\n    const gain = ctx.createGain()\n\n    osc.type = 'sine'\n    osc.frequency.setValueAtTime(1400 * pitch, now)\n    osc.frequency.exponentialRampToValueAtTime(350 * pitch, now + 0.012)\n\n    gain.gain.setValueAtTime(volume, now)\n    gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.012)\n\n    osc.connect(gain)\n    gain.connect(ctx.destination)\n\n    osc.start(now)\n    osc.stop(now + 0.012)\n  } catch {}\n}\n\nexport function playClickSound(volume = 0.2, pitch = 1.0) {\n  if (muted) return\n  try {\n    const ctx = getAudioContext()\n    if (!ctx) return\n    const now = ctx.currentTime\n\n    const osc = ctx.createOscillator()\n    const gain = ctx.createGain()\n\n    osc.type = 'triangle'\n    osc.frequency.setValueAtTime(850 * pitch, now)\n    osc.frequency.exponentialRampToValueAtTime(160 * pitch, now + 0.02)\n\n    gain.gain.setValueAtTime(volume, now)\n    gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.02)\n\n    osc.connect(gain)\n    gain.connect(ctx.destination)\n\n    osc.start(now)\n    osc.stop(now + 0.02)\n  } catch {}\n}\n\nexport function playTickSound(volume = 0.1, pitch = 1.5) {\n  if (muted) return\n  try {\n    const ctx = getAudioContext()\n    if (!ctx) return\n    const now = ctx.currentTime\n\n    const osc = ctx.createOscillator()\n    const gain = ctx.createGain()\n\n    osc.type = 'sine'\n    osc.frequency.setValueAtTime(1800 * pitch, now)\n    osc.frequency.exponentialRampToValueAtTime(450 * pitch, now + 0.01)\n\n    gain.gain.setValueAtTime(volume, now)\n    gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.01)\n\n    osc.connect(gain)\n    gain.connect(ctx.destination)\n\n    osc.start(now)\n    osc.stop(now + 0.01)\n  } catch {}\n}\n\nexport function playBounceSound(volume = 0.3, pitch = 1.0) {\n  if (muted) return\n  try {\n    const ctx = getAudioContext()\n    if (!ctx) return\n    const now = ctx.currentTime\n\n    const osc = ctx.createOscillator()\n    const gain = ctx.createGain()\n\n    osc.type = 'sine'\n    osc.frequency.setValueAtTime(260 * pitch, now)\n    osc.frequency.exponentialRampToValueAtTime(650 * pitch, now + 0.04)\n    osc.frequency.exponentialRampToValueAtTime(190 * pitch, now + 0.14)\n\n    gain.gain.setValueAtTime(volume, now)\n    gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.14)\n\n    osc.connect(gain)\n    gain.connect(ctx.destination)\n\n    osc.start(now)\n    osc.stop(now + 0.14)\n\n    const subOsc = ctx.createOscillator()\n    const subGain = ctx.createGain()\n\n    subOsc.type = 'triangle'\n    subOsc.frequency.setValueAtTime(420 * pitch, now)\n    subOsc.frequency.exponentialRampToValueAtTime(120 * pitch, now + 0.05)\n\n    subGain.gain.setValueAtTime(volume * 0.7, now)\n    subGain.gain.exponentialRampToValueAtTime(0.0001, now + 0.05)\n\n    subOsc.connect(subGain)\n    subGain.connect(ctx.destination)\n\n    subOsc.start(now)\n    subOsc.stop(now + 0.05)\n  } catch {}\n}\n"
    }
  ],
  "type": "registry:ui"
}
