{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "bloom-color-picker",
  "title": "Bloom Color Picker",
  "description": "A flower-inspired color picker — a swatch that blooms open into petals. Zero dependencies.",
  "files": [
    {
      "path": "packages/react/src/index.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport \"./style.css\";\n\nimport { deriveFromHex, normalizeHex, shadeOf } from \"./color\";\nimport { bloomPalettes } from \"./palettes\";\nimport type { BloomColorPickerPart, BloomColorPickerProps } from \"./types\";\nimport { useControllableState } from \"./use-controllable-state\";\n\nconst TAU = Math.PI * 2;\n\n// Base geometry, defined at size = 50 (closed swatch diameter). Everything scales linearly.\nconst BASE_SWATCH = 50;\nconst BLOOM_SIZE = 280;\nconst PETAL_SIZE = 54;\nconst OUTER_RADIUS = 78;\nconst INNER_RADIUS = 42;\n\n// Brightness arc (SVG canvas units — the svg element scales as a whole)\nconst ARC_CANVAS = 360;\nconst ARC_C = ARC_CANVAS / 2;\nconst ARC_RADIUS = 170;\nconst ARC_HALF_SPAN = 26; // degrees above & below 3 o'clock\nconst ARC_STROKE = 20;\n\ninterface Petal {\n   key: string;\n   x: number;\n   y: number;\n   color: string;\n   order: number;\n}\n\nfunction buildPetals(outer: string[], inner: string[]): Petal[] {\n   const raw: Array<Omit<Petal, \"order\"> & { radius: number; angleNorm: number }> = [];\n\n   // Outer ring first so inner petals overlap on top.\n   outer.forEach((color, i) => {\n      const angle = (i / outer.length) * TAU - Math.PI / 2;\n      raw.push({\n         key: `o${i}`,\n         x: Math.cos(angle) * OUTER_RADIUS,\n         y: Math.sin(angle) * OUTER_RADIUS,\n         color,\n         radius: OUTER_RADIUS,\n         angleNorm: i / outer.length,\n      });\n   });\n\n   inner.forEach((color, i) => {\n      const angle = (i / inner.length) * TAU - Math.PI / 2;\n      raw.push({\n         key: `i${i}`,\n         x: Math.cos(angle) * INNER_RADIUS,\n         y: Math.sin(angle) * INNER_RADIUS,\n         color,\n         radius: INNER_RADIUS,\n         angleNorm: i / inner.length,\n      });\n   });\n\n   // White center on top\n   raw.push({ key: \"center\", x: 0, y: 0, color: \"#FFFFFF\", radius: 0, angleNorm: 0 });\n\n   // Spiral reveal order: radius + angle so it winds outward (rings interleave)\n   const orderOf = new Map<string, number>();\n   raw.map((p) => ({ key: p.key, m: p.radius / OUTER_RADIUS + p.angleNorm }))\n      .sort((a, b) => a.m - b.m)\n      .forEach((e, idx) => orderOf.set(e.key, idx));\n\n   return raw.map((p) => ({\n      key: p.key,\n      x: p.x,\n      y: p.y,\n      color: p.color,\n      order: orderOf.get(p.key)!,\n   }));\n}\n\nfunction arcPath(): string {\n   const a0 = (-ARC_HALF_SPAN * Math.PI) / 180;\n   const a1 = (ARC_HALF_SPAN * Math.PI) / 180;\n   const x0 = ARC_C + ARC_RADIUS * Math.cos(a0);\n   const y0 = ARC_C + ARC_RADIUS * Math.sin(a0);\n   const x1 = ARC_C + ARC_RADIUS * Math.cos(a1);\n   const y1 = ARC_C + ARC_RADIUS * Math.sin(a1);\n   return `M${x0} ${y0} A ${ARC_RADIUS} ${ARC_RADIUS} 0 0 1 ${x1} ${y1}`;\n}\n\nconst ARC_PATH = arcPath();\n\nfunction cx(...parts: Array<string | false | null | undefined>): string {\n   return parts.filter(Boolean).join(\" \");\n}\n\nconst FALLBACK_HEX = \"#F5B81E\";\n\nexport function BloomColorPicker(props: BloomColorPickerProps) {\n   const {\n      value: valueProp,\n      defaultValue = FALLBACK_HEX,\n      onChange,\n      open: openProp,\n      defaultOpen = false,\n      onOpenChange,\n      palette = \"warm\",\n      outerColors,\n      innerColors,\n      size = 28,\n      disabled = false,\n      hexInput = true,\n      inputVariant = \"split\",\n      motion = \"subtle\",\n      theme = \"light\",\n      className,\n      classNames,\n      \"aria-label\": ariaLabel = \"Pick a color\",\n   } = props;\n\n   const paletteColors = bloomPalettes[palette] ?? bloomPalettes.warm;\n   const resolvedOuterColors = outerColors ?? paletteColors.outer;\n   const resolvedInnerColors = innerColors ?? paletteColors.inner;\n\n   const scale = size / BASE_SWATCH;\n   const part = (name: BloomColorPickerPart) => classNames?.[name];\n\n   // \"none\" swaps the full spring choreography for one quick uniform fade —\n   // these match the 160ms duration forced via [data-motion=\"none\"] in style.css.\n   const instant = motion === \"none\";\n   const FADE_MS = 160;\n   const PRESS_MS = instant ? FADE_MS : 140;\n   const BLOOM_DELAY_MS = instant ? FADE_MS : 1300;\n   const CLOSE_PETALS_MS = instant ? 0 : 110;\n   const CLOSE_UNMOUNT_MS = instant ? FADE_MS : 480;\n\n   // Outline rings scale with the picker so proportions match the original at any size\n   const ring = (color: string): React.CSSProperties => ({\n      outline: `${2 * scale}px solid ${color}`,\n      outlineOffset: `${-2 * scale}px`,\n   });\n\n   const [rawValue, setValue] = useControllableState(valueProp, defaultValue, onChange);\n   const hex = normalizeHex(rawValue) ?? FALLBACK_HEX;\n   const { base, lightPos } = React.useMemo(() => deriveFromHex(hex), [hex]);\n\n   // Draft text for the optional hex input: lets the field hold invalid/partial\n   // text while typing, but stays in sync when the color changes elsewhere\n   // (petal pick, arc drag) without fighting an in-progress keystroke.\n   const [hexDraft, setHexDraft] = React.useState(hex);\n   React.useEffect(() => {\n      if (normalizeHex(hexDraft) !== hex) setHexDraft(hex);\n   }, [hex]);\n\n   const [open, setOpen] = useControllableState(openProp, defaultOpen, onOpenChange);\n\n   // Presentation lifecycle: the bloom stays mounted through the two-phase close\n   // (circles collapse at 0ms, petals converge at 110ms, unmount at 480ms).\n   const [rendered, setRendered] = React.useState(open);\n   const [closing, setClosing] = React.useState(false);\n   const [petalsHome, setPetalsHome] = React.useState(false);\n   const [bloomed, setBloomed] = React.useState(false); // spiral entrance finished\n   const [pressing, setPressing] = React.useState(false);\n   const [dragging, setDragging] = React.useState(false);\n   const [hovered, setHovered] = React.useState<string | null>(null);\n\n   const renderedRef = React.useRef(rendered);\n   renderedRef.current = rendered;\n\n   const containerRef = React.useRef<HTMLDivElement>(null);\n   const dishRef = React.useRef<HTMLDivElement>(null);\n   const svgRef = React.useRef<SVGSVGElement>(null);\n\n   const gradientId = React.useId();\n\n   const petals = React.useMemo(\n      () => buildPetals(resolvedOuterColors, resolvedInnerColors),\n      [resolvedOuterColors, resolvedInnerColors]\n   );\n\n   React.useEffect(() => {\n      const timers: number[] = [];\n      if (open) {\n         setRendered(true);\n         setClosing(false);\n         setPetalsHome(false);\n         timers.push(window.setTimeout(() => setBloomed(true), BLOOM_DELAY_MS));\n      } else {\n         setBloomed(false);\n         setHovered(null);\n         if (renderedRef.current) {\n            setClosing(true);\n            timers.push(window.setTimeout(() => setPetalsHome(true), CLOSE_PETALS_MS));\n            timers.push(\n               window.setTimeout(() => {\n                  setRendered(false);\n                  setClosing(false);\n                  setPetalsHome(false);\n               }, CLOSE_UNMOUNT_MS)\n            );\n         }\n      }\n      return () => timers.forEach(clearTimeout);\n   }, [open, BLOOM_DELAY_MS, CLOSE_PETALS_MS, CLOSE_UNMOUNT_MS]);\n\n   // Close on outside click / Escape\n   React.useEffect(() => {\n      if (!open) return;\n      const onDown = (e: PointerEvent) => {\n         if (containerRef.current && !containerRef.current.contains(e.target as Node)) {\n            setOpen(false);\n         }\n      };\n      const onKey = (e: KeyboardEvent) => {\n         if (e.key === \"Escape\") setOpen(false);\n      };\n      document.addEventListener(\"pointerdown\", onDown);\n      document.addEventListener(\"keydown\", onKey);\n      return () => {\n         document.removeEventListener(\"pointerdown\", onDown);\n         document.removeEventListener(\"keydown\", onKey);\n      };\n   }, [open, setOpen]);\n\n   const openPicker = () => {\n      if (disabled) return;\n      setPressing(true);\n      window.setTimeout(() => {\n         setPressing(false);\n         setOpen(true);\n      }, PRESS_MS);\n   };\n\n   // Single source of truth for hover: pick the nearest petal under the pointer.\n   // Avoids missed enter/leave events between overlapping petals.\n   const handleDishMove = (e: React.PointerEvent) => {\n      if (!bloomed || !dishRef.current) return;\n      const rect = dishRef.current.getBoundingClientRect();\n      const px = e.clientX - rect.left - rect.width / 2;\n      const py = e.clientY - rect.top - rect.height / 2;\n      let best: string | null = null;\n      let bestDist = ((PETAL_SIZE * scale) / 2) ** 2;\n      for (const p of petals) {\n         const d = (px - p.x * scale) ** 2 + (py - p.y * scale) ** 2;\n         if (d <= bestDist) {\n            bestDist = d;\n            best = p.key;\n         }\n      }\n      setHovered(best);\n   };\n\n   const shade = hex;\n   const ringColor = `color-mix(in srgb, color-mix(in srgb, ${shade}, #000 30%) 14%, transparent)`;\n\n   // Knob position along the arc\n   const knobAngle = ((-ARC_HALF_SPAN + lightPos * 2 * ARC_HALF_SPAN) * Math.PI) / 180;\n   const knobX = ARC_C + ARC_RADIUS * Math.cos(knobAngle);\n   const knobY = ARC_C + ARC_RADIUS * Math.sin(knobAngle);\n\n   const onKnobDown = (e: React.PointerEvent) => {\n      e.preventDefault();\n      setDragging(true);\n      const dragBase = base; // hue/saturation are stable for the whole drag\n      const updateFromPointer = (clientX: number, clientY: number) => {\n         const svg = svgRef.current;\n         if (!svg) return;\n         const rect = svg.getBoundingClientRect();\n         const lx = (clientX - rect.left) * (ARC_CANVAS / rect.width);\n         const ly = (clientY - rect.top) * (ARC_CANVAS / rect.height);\n         let deg = (Math.atan2(ly - ARC_C, lx - ARC_C) * 180) / Math.PI;\n         deg = Math.max(-ARC_HALF_SPAN, Math.min(ARC_HALF_SPAN, deg));\n         const pos = (deg + ARC_HALF_SPAN) / (2 * ARC_HALF_SPAN);\n         setValue(shadeOf(dragBase, pos));\n      };\n      updateFromPointer(e.clientX, e.clientY);\n      const move = (ev: PointerEvent) => updateFromPointer(ev.clientX, ev.clientY);\n      const up = () => {\n         setDragging(false);\n         window.removeEventListener(\"pointermove\", move);\n         window.removeEventListener(\"pointerup\", up);\n      };\n      window.addEventListener(\"pointermove\", move);\n      window.addEventListener(\"pointerup\", up);\n   };\n\n   const pickPetal = (color: string) => {\n      const normalized = normalizeHex(color);\n      if (!normalized) return;\n      // Round-trip through the internal model so the reported hex matches\n      // what the knob/gradient will display for this petal.\n      const { base: petalBase, lightPos: petalPos } = deriveFromHex(normalized);\n      setValue(shadeOf(petalBase, petalPos));\n   };\n\n   return (\n      <div\n         ref={containerRef}\n         className={cx(\"bcp\", dragging && \"bcp--dragging\", className, part(\"root\"))}\n         style={{ \"--bcp-scale\": scale } as React.CSSProperties}\n         data-slot=\"bcp-root\"\n         data-state={open ? \"open\" : \"closed\"}\n         data-disabled={disabled || undefined}\n         data-motion={motion}\n         data-input-variant={hexInput ? inputVariant : undefined}\n         data-theme={theme !== \"auto\" ? theme : undefined}\n         onPointerMove={handleDishMove}\n         onPointerLeave={() => setHovered(null)}\n      >\n         {rendered ? (\n            <div className=\"bcp__slot\">\n               <svg\n                  ref={svgRef}\n                  className={cx(\"bcp__arc\", closing && \"bcp__arc--closing\", part(\"arc\"))}\n                  data-slot=\"bcp-arc\"\n                  width={ARC_CANVAS * scale}\n                  height={ARC_CANVAS * scale}\n                  viewBox={`0 0 ${ARC_CANVAS} ${ARC_CANVAS}`}\n                  fill=\"none\"\n                  xmlns=\"http://www.w3.org/2000/svg\"\n               >\n                  <defs>\n                     <linearGradient\n                        id={gradientId}\n                        gradientUnits=\"userSpaceOnUse\"\n                        x1={ARC_C}\n                        y1={ARC_C - ARC_RADIUS * Math.sin((ARC_HALF_SPAN * Math.PI) / 180)}\n                        x2={ARC_C}\n                        y2={ARC_C + ARC_RADIUS * Math.sin((ARC_HALF_SPAN * Math.PI) / 180)}\n                     >\n                        <stop offset=\"0\" stopColor=\"#ffffff\" />\n                        <stop offset=\"0.35\" stopColor={base} />\n                        <stop offset=\"0.65\" stopColor={base} />\n                        <stop offset=\"1\" stopColor=\"#000000\" />\n                     </linearGradient>\n                  </defs>\n                  <path\n                     d={ARC_PATH}\n                     stroke={ringColor}\n                     strokeWidth={ARC_STROKE + 4}\n                     strokeLinecap=\"round\"\n                  />\n                  <path\n                     d={ARC_PATH}\n                     stroke={`url(#${gradientId})`}\n                     strokeWidth={ARC_STROKE}\n                     strokeLinecap=\"round\"\n                  />\n\n                  {/* Knob: selected shade with a white ring */}\n                  <g\n                     className={cx(\"bcp__knob\", part(\"knob\"))}\n                     data-slot=\"bcp-knob\"\n                     onPointerDown={onKnobDown}\n                     style={{ cursor: dragging ? \"grabbing\" : \"grab\" }}\n                  >\n                     <circle className=\"bcp__knob-halo\" cx={knobX} cy={knobY} fill=\"#fff\" />\n                     <circle className=\"bcp__knob-core\" cx={knobX} cy={knobY} fill={shade} />\n                  </g>\n               </svg>\n\n               <div\n                  className={cx(\"bcp__bloom\", closing && \"bcp__bloom--closing\", part(\"bloom\"))}\n                  data-slot=\"bcp-bloom\"\n                  style={{ background: shade, ...ring(ringColor) }}\n               >\n                  <div\n                     ref={dishRef}\n                     className={cx(\"bcp__dish\", part(\"dish\"))}\n                     data-slot=\"bcp-dish\"\n                     style={ring(ringColor)}\n                  />\n               </div>\n\n               {/* Petals — siblings of the bloom so they outlive the circle close */}\n               {petals.map((p) => (\n                  <button\n                     key={p.key}\n                     type=\"button\"\n                     className={cx(\n                        \"bcp__petal\",\n                        hovered === p.key && !petalsHome && \"bcp__petal--hovered\",\n                        petalsHome && \"bcp__petal--home\",\n                        part(\"petal\")\n                     )}\n                     data-slot=\"bcp-petal\"\n                     style={\n                        {\n                           width: PETAL_SIZE * scale,\n                           height: PETAL_SIZE * scale,\n                           marginLeft: (-PETAL_SIZE / 2) * scale,\n                           marginTop: (-PETAL_SIZE / 2) * scale,\n                           left: `calc(50% + ${p.x * scale}px)`,\n                           top: `calc(50% + ${p.y * scale}px)`,\n                           background: p.color,\n                           ...ring(\n                              `color-mix(in srgb, color-mix(in srgb, ${p.color}, #000 30%) 18%, transparent)`\n                           ),\n                           \"--bcp-from-x\": `${-p.x * scale}px`,\n                           \"--bcp-from-y\": `${-p.y * scale}px`,\n                           \"--bcp-petal-delay\": `${0.06 + p.order * 0.022}s`,\n                        } as React.CSSProperties\n                     }\n                     onClick={() => pickPetal(p.color)}\n                     aria-label={p.color}\n                  />\n               ))}\n            </div>\n         ) : (\n            <button\n               type=\"button\"\n               className={cx(\"bcp__swatch\", pressing && \"bcp__swatch--pressing\", part(\"swatch\"))}\n               data-slot=\"bcp-swatch\"\n               style={{ backgroundColor: shade }}\n               onClick={openPicker}\n               disabled={disabled}\n               aria-label={ariaLabel}\n               aria-haspopup=\"dialog\"\n               aria-expanded={open}\n            />\n         )}\n\n         {hexInput && (\n            <div className=\"bcp__input-wrap\">\n               <input\n                  type=\"text\"\n                  size={7}\n                  className={cx(\"bcp__input\", part(\"input\"))}\n                  data-slot=\"bcp-input\"\n                  value={hexDraft}\n                  onChange={(e) => {\n                     const raw = e.target.value.toUpperCase();\n                     const hasHash = raw.startsWith(\"#\");\n                     const digits = raw.replace(/[^0-9A-F]/g, \"\").slice(0, 6);\n                     const next = (hasHash ? \"#\" : \"\") + digits;\n                     setHexDraft(next);\n                     const valid = normalizeHex(next);\n                     if (valid) setValue(valid);\n                  }}\n                  disabled={disabled}\n                  spellCheck={false}\n                  placeholder=\"#RRGGBB\"\n                  aria-label=\"Hex color value\"\n               />\n            </div>\n         )}\n      </div>\n   );\n}\n\nexport default BloomColorPicker;\n\nexport type {\n   BloomColorPickerInputVariant,\n   BloomColorPickerMotion,\n   BloomColorPickerPart,\n   BloomColorPickerProps,\n   BloomColorPickerTheme,\n} from \"./types\";\nexport { deriveFromHex, hexToHsl, hslToHex, normalizeHex, shadeOf } from \"./color\";\nexport {\n   bloomPalettes,\n   defaultInnerColors,\n   defaultOuterColors,\n   type BloomColorPickerPalette,\n   type BloomColorPickerPaletteColors,\n} from \"./palettes\";\n",
      "type": "registry:component",
      "target": "@components/bloom-color-picker/index.tsx"
    },
    {
      "path": "packages/react/src/types.ts",
      "content": "import type { BloomColorPickerPalette } from \"./palettes\";\n\nexport type BloomColorPickerPart =\n   | \"root\"\n   | \"swatch\"\n   | \"bloom\"\n   | \"dish\"\n   | \"petal\"\n   | \"arc\"\n   | \"knob\"\n   | \"input\";\n\nexport type BloomColorPickerMotion = \"none\" | \"subtle\" | \"bouncy\";\n\nexport type BloomColorPickerTheme = \"auto\" | \"light\" | \"dark\";\n\nexport type BloomColorPickerInputVariant = \"split\" | \"grouped\";\n\nexport interface BloomColorPickerProps {\n   /** Controlled hex value, e.g. \"#F5B81E\". */\n   value?: string;\n   /** Initial hex value when uncontrolled. @default \"#F5B81E\" */\n   defaultValue?: string;\n   /** Fired with the new hex (uppercase \"#RRGGBB\") on every petal pick or brightness drag. */\n   onChange?: (hex: string) => void;\n\n   /** Controlled open state of the bloom. */\n   open?: boolean;\n   /** Initial open state when uncontrolled. @default false */\n   defaultOpen?: boolean;\n   /** Fired when the picker requests to open/close (swatch click, outside click, Escape). */\n   onOpenChange?: (open: boolean) => void;\n\n   /**\n    * A built-in petal color scheme: \"warm\" (default), \"ocean\", \"blossom\", \"pastel\".\n    * Ignored for a ring where `outerColors`/`innerColors` is explicitly passed.\n    */\n   palette?: BloomColorPickerPalette;\n   /** Outer ring petal colors, clockwise from the top. Hex only. Overrides `palette`. */\n   outerColors?: string[];\n   /** Inner ring petal colors, clockwise from the top. Hex only. Overrides `palette`. */\n   innerColors?: string[];\n\n   /**\n    * Diameter of the closed swatch in px; the whole bloom scales proportionally.\n    * @default 28\n    */\n   size?: number;\n\n   /** Disables opening the picker. @default false */\n   disabled?: boolean;\n\n   /**\n    * Shows an editable hex text field next to the closed swatch. Typed\n    * values are validated live; invalid characters can't be typed at all.\n    * Set to `false` to hide it. @default true\n    */\n   hexInput?: boolean;\n\n   /**\n    * Layout for the swatch + hex input: \"split\" keeps them as separate\n    * elements (default), \"grouped\" wraps both in one shared box.\n    * Ignored when `hexInput` is false.\n    * @default \"split\"\n    */\n   inputVariant?: BloomColorPickerInputVariant;\n\n   /**\n    * Spring intensity for the open/close and pick animations.\n    * \"none\" disables animation entirely (instant open/close, respected\n    * regardless of the visitor's OS motion setting).\n    * @default \"subtle\"\n    */\n   motion?: BloomColorPickerMotion;\n\n   /**\n    * \"light\" or \"dark\" pins the picker's own chrome (input field, dish,\n    * shadows — not petal colors, which come from `palette`/`outerColors`/\n    * `innerColors`) to that theme. \"auto\" follows the visitor's system/OS\n    * `prefers-color-scheme` setting instead of any explicit override.\n    * @default \"light\"\n    */\n   theme?: BloomColorPickerTheme;\n\n   /** Class applied to the root element. */\n   className?: string;\n   /** Per-part class overrides for restyling. */\n   classNames?: Partial<Record<BloomColorPickerPart, string>>;\n\n   /** Accessible label for the closed swatch button. @default \"Pick a color\" */\n   \"aria-label\"?: string;\n}\n",
      "type": "registry:file",
      "target": "@components/bloom-color-picker/types.ts"
    },
    {
      "path": "packages/react/src/palettes.ts",
      "content": "export interface BloomColorPickerPaletteColors {\n   outer: string[];\n   inner: string[];\n}\n\n// \"warm\" — curated warm color wheel, clockwise from the top. The original palette.\nconst warmOuter = [\n   \"#F7C13F\", // yellow (top)\n   \"#F2A23C\", // amber\n   \"#EE8440\", // orange\n   \"#E96544\", // coral\n   \"#E84C3F\", // red\n   \"#E03E66\", // rose\n   \"#D23C92\", // magenta (bottom)\n   \"#A24FC8\", // purple\n   \"#7B5FD4\", // violet\n   \"#4E72D6\", // blue\n   \"#3DA1B8\", // teal\n   \"#5FB95B\", // green\n];\nconst warmInner = [\n   \"#F6E6A4\", // pale yellow (top)\n   \"#F4D0B0\", // pale peach\n   \"#F1C1C4\", // pale red/pink\n   \"#E3C4DE\", // pale magenta\n   \"#CCC9EC\", // pale violet/blue\n   \"#C6E0C9\", // pale teal/green\n];\n\n// \"ocean\" — cool blues, teals, and greens.\nconst oceanOuter = [\n   \"#3D9BE0\",\n   \"#3DB8D6\",\n   \"#3DCBB8\",\n   \"#4ED68B\",\n   \"#5FB95B\",\n   \"#8FCB4E\",\n   \"#C9D63D\",\n   \"#8B9BE0\",\n   \"#7B5FD4\",\n   \"#5F72E0\",\n   \"#4E86E8\",\n   \"#3DAEE0\",\n];\nconst oceanInner = [\"#BBDFF5\", \"#B8E8E0\", \"#C6E8C0\", \"#DCEDB5\", \"#C9CCF0\", \"#B5D2F0\"];\n\n// \"blossom\" — warm pinks and magentas.\nconst blossomOuter = [\n   \"#F7C13F\",\n   \"#F5A852\",\n   \"#F28B66\",\n   \"#EE6E7B\",\n   \"#E85A94\",\n   \"#E04FB0\",\n   \"#C94FD1\",\n   \"#A45FE0\",\n   \"#E0559A\",\n   \"#F06880\",\n   \"#F5895E\",\n   \"#F7A94A\",\n];\nconst blossomInner = [\"#F8E7B5\", \"#F7D6BB\", \"#F5C6CC\", \"#EFC3E3\", \"#E0C6F0\", \"#F5CBBE\"];\n\n// \"pastel\" — soft, low-saturation tones throughout both rings.\nconst pastelOuter = [\n   \"#F5D9A8\",\n   \"#F3C9B0\",\n   \"#F0BCC0\",\n   \"#E7BCDA\",\n   \"#D4C0EC\",\n   \"#C3C6EE\",\n   \"#BDD6EA\",\n   \"#BDE2DD\",\n   \"#C4E6C4\",\n   \"#DCE9B8\",\n   \"#EEE3AE\",\n   \"#F2D6A6\",\n];\nconst pastelInner = [\"#FBEFD8\", \"#FAE4DC\", \"#F8DDE2\", \"#EFDCF0\", \"#E4DFF7\", \"#DDE8F6\"];\n\nexport const bloomPalettes = {\n   warm: { outer: warmOuter, inner: warmInner },\n   ocean: { outer: oceanOuter, inner: oceanInner },\n   blossom: { outer: blossomOuter, inner: blossomInner },\n   pastel: { outer: pastelOuter, inner: pastelInner },\n} satisfies Record<string, BloomColorPickerPaletteColors>;\n\nexport type BloomColorPickerPalette = keyof typeof bloomPalettes;\n\n// Preserved for backwards compat with earlier exports.\nexport const defaultOuterColors = warmOuter;\nexport const defaultInnerColors = warmInner;\n",
      "type": "registry:file",
      "target": "@components/bloom-color-picker/palettes.ts"
    },
    {
      "path": "packages/react/src/color.ts",
      "content": "export function hexToRgb(h: string): [number, number, number] {\n   h = h.replace(\"#\", \"\");\n   if (h.length === 3)\n      h = h\n         .split(\"\")\n         .map((c) => c + c)\n         .join(\"\");\n   const n = parseInt(h, 16);\n   return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n}\n\nexport function rgbToHex(r: number, g: number, b: number): string {\n   return (\n      \"#\" +\n      [r, g, b]\n         .map((v) =>\n            Math.round(Math.min(255, Math.max(0, v)))\n               .toString(16)\n               .toUpperCase()\n               .padStart(2, \"0\")\n         )\n         .join(\"\")\n   );\n}\n\nexport function mixHex(hex: string, target: string, t: number): string {\n   const a = hexToRgb(hex);\n   const b = hexToRgb(target);\n   return rgbToHex(a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t);\n}\n\n// pos 0 = lightest (white), 0.5 = pure, 1 = darkest (black)\nexport function shadeOf(base: string, pos: number): string {\n   if (pos <= 0.5) return mixHex(base, \"#ffffff\", (0.5 - pos) / 0.5);\n   return mixHex(base, \"#000000\", (pos - 0.5) / 0.5);\n}\n\nexport function hexToHsl(hex: string): [number, number, number] {\n   const [r, g, b] = hexToRgb(hex).map((v) => v / 255);\n   const max = Math.max(r, g, b);\n   const min = Math.min(r, g, b);\n   const l = (max + min) / 2;\n   let h = 0;\n   let s = 0;\n   if (max !== min) {\n      const d = max - min;\n      s = l > 0.5 ? d / (2 - max - min) : d / (max + min);\n      if (max === r) h = (g - b) / d + (g < b ? 6 : 0);\n      else if (max === g) h = (b - r) / d + 2;\n      else h = (r - g) / d + 4;\n      h *= 60;\n   }\n   return [h, s, l];\n}\n\nexport function hslToHex(h: number, s: number, l: number): string {\n   h /= 360;\n   let r: number;\n   let g: number;\n   let b: number;\n   if (s === 0) {\n      r = g = b = l;\n   } else {\n      const hue2rgb = (p: number, q: number, t: number) => {\n         if (t < 0) t += 1;\n         if (t > 1) t -= 1;\n         if (t < 1 / 6) return p + (q - p) * 6 * t;\n         if (t < 1 / 2) return q;\n         if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;\n         return p;\n      };\n      const q = l < 0.5 ? l * (1 + s) : l + s - l * s;\n      const p = 2 * l - q;\n      r = hue2rgb(p, q, h + 1 / 3);\n      g = hue2rgb(p, q, h);\n      b = hue2rgb(p, q, h - 1 / 3);\n   }\n   return rgbToHex(r * 255, g * 255, b * 255);\n}\n\n/** \"#abc\" | \"abc\" | \"#AABBCC\" -> \"#AABBCC\"; returns null when not a valid hex color. */\nexport function normalizeHex(input: string): string | null {\n   let v = input.trim().replace(/^#/, \"\");\n   if (/^[0-9a-fA-F]{3}$/.test(v))\n      v = v\n         .split(\"\")\n         .map((c) => c + c)\n         .join(\"\");\n   if (!/^[0-9a-fA-F]{6}$/.test(v)) return null;\n   return \"#\" + v.toUpperCase();\n}\n\n/**\n * Decompose a hex into the picker's internal model: the pure hue at 50% lightness\n * plus a position on the light/dark arc. shadeOf(base, lightPos) reproduces the hex.\n */\nexport function deriveFromHex(hex: string): { base: string; lightPos: number } {\n   const [h, s, l] = hexToHsl(hex);\n   return {\n      base: hslToHex(h, s, 0.5),\n      lightPos: Math.min(1, Math.max(0, 1 - l)),\n   };\n}\n",
      "type": "registry:file",
      "target": "@components/bloom-color-picker/color.ts"
    },
    {
      "path": "packages/react/src/use-controllable-state.ts",
      "content": "import * as React from \"react\";\n\n/**\n * Controlled/uncontrolled state: when `prop` is provided the component follows it\n * and only reports changes through `onChange`; otherwise state is kept internally.\n */\nexport function useControllableState<T>(\n   prop: T | undefined,\n   defaultProp: T,\n   onChange?: (value: T) => void\n): [T, (next: T) => void] {\n   const [internal, setInternal] = React.useState(defaultProp);\n   const isControlled = prop !== undefined;\n   const value = isControlled ? prop : internal;\n\n   const onChangeRef = React.useRef(onChange);\n   React.useEffect(() => {\n      onChangeRef.current = onChange;\n   });\n\n   const setValue = React.useCallback(\n      (next: T) => {\n         if (!isControlled) setInternal(next);\n         onChangeRef.current?.(next);\n      },\n      [isControlled]\n   );\n\n   return [value, setValue];\n}\n",
      "type": "registry:hook",
      "target": "@components/bloom-color-picker/use-controllable-state.ts"
    },
    {
      "path": "packages/react/src/style.css",
      "content": "/* bloom-color-picker — zero-dependency styles.\n   Spring easings are pre-computed linear() curves matching the original\n   motion springs (stiffness/damping noted on each). */\n\n.bcp {\n   /* Fallback timings for browsers without linear() support (Safari < 17.2,\n      Chrome < 113, Firefox < 112): shorter durations, spring-ish beziers. */\n   --bcp-dur-bloom: 450ms;\n   --bcp-ease-bloom: cubic-bezier(0.22, 1, 0.36, 1);\n   --bcp-dur-dish: 500ms;\n   --bcp-ease-dish: cubic-bezier(0.34, 1.3, 0.64, 1);\n   --bcp-dur-arc: 500ms;\n   --bcp-ease-arc: cubic-bezier(0.25, 0.1, 0.25, 1);\n   --bcp-dur-petal: 550ms;\n   --bcp-ease-petal: cubic-bezier(0.22, 1, 0.36, 1);\n   --bcp-dur-petal-hover: 400ms;\n   --bcp-ease-petal-hover: cubic-bezier(0.34, 1.56, 0.64, 1);\n   --bcp-dur-knob: 130ms;\n   --bcp-ease-knob: cubic-bezier(0.25, 0.1, 0.25, 1);\n   --bcp-dur-press: 300ms;\n   --bcp-ease-press: cubic-bezier(0.34, 1.56, 0.64, 1);\n\n   /* Focus-pull radii, kept as variables so they can be turned off where they\n      are too expensive to be worth it — see the coarse-pointer block at the end\n      of this file. `transform` and `opacity` are composited, but `blur()` forces\n      a re-raster every frame, and these layers all animate at once. */\n   --bcp-petal-blur-in: 12px;\n   --bcp-petal-blur-out: 3px;\n   --bcp-bloom-blur: 3px;\n   --bcp-dish-blur: 8px;\n   --bcp-arc-blur: 8px;\n\n   --bcp-scale: 1;\n\n   /* public theming variables — override any of these to restyle without\n      touching the component's own classes. Defaults match the original look. */\n   --bcp-color-focus: #4e72d6;\n   --bcp-swatch-ring: #fff;\n   --bcp-swatch-shadow: rgba(0, 0, 0, 0.08);\n   --bcp-bloom-shadow: rgba(0, 0, 0, 0.12);\n   --bcp-dish-bg: rgba(233, 234, 236, 0.8);\n   --bcp-petal-shadow: rgba(0, 0, 0, 0.06);\n   --bcp-input-bg: #f2f2f4;\n   --bcp-input-border: #e4e4e9;\n   --bcp-input-color: #8a8a92;\n   --bcp-input-color-focus: #1a1a1a;\n\n   position: relative;\n   isolation: isolate;\n   display: flex;\n   align-items: center;\n   justify-content: center;\n}\n\n/* dark theme — via an explicit theme=\"dark\" prop (sets data-theme\n   directly, applies regardless of the OS setting) or theme=\"auto\"\n   (no data-theme attribute) following prefers-color-scheme. Shadows get\n   bumped opacity since a plain black shadow barely reads against an\n   already-dark surrounding page. */\n.bcp[data-theme=\"dark\"] {\n   --bcp-swatch-ring: rgba(255, 255, 255, 0.6);\n   --bcp-swatch-shadow: rgba(0, 0, 0, 0.4);\n   --bcp-bloom-shadow: rgba(0, 0, 0, 0.45);\n   --bcp-petal-shadow: rgba(0, 0, 0, 0.18);\n   --bcp-dish-bg: rgba(40, 40, 46, 0.28);\n   --bcp-input-bg: #2a2a2f;\n   --bcp-input-border: #3a3a40;\n   --bcp-input-color: #a0a0a8;\n   --bcp-input-color-focus: #f2f2f4;\n}\n\n/* theme=\"auto\" (no data-theme attribute) — follow the OS setting instead.\n   :not([data-theme=\"light\"]) keeps an explicit theme=\"light\" override\n   winning even when the OS is dark. */\n@media (prefers-color-scheme: dark) {\n   .bcp:not([data-theme=\"light\"]) {\n      --bcp-swatch-ring: rgba(255, 255, 255, 0.6);\n      --bcp-swatch-shadow: rgba(0, 0, 0, 0.4);\n      --bcp-bloom-shadow: rgba(0, 0, 0, 0.45);\n      --bcp-petal-shadow: rgba(0, 0, 0, 0.18);\n      --bcp-dish-bg: rgba(40, 40, 46, 0.28);\n      --bcp-input-bg: #2a2a2f;\n      --bcp-input-border: #3a3a40;\n      --bcp-input-color: #a0a0a8;\n      --bcp-input-color-focus: #f2f2f4;\n   }\n}\n\n/* Spring easings: pre-computed linear() curves matching the original motion\n   springs (stiffness/damping noted on each). */\n@supports (transition-timing-function: linear(0, 1)) {\n   .bcp {\n      /* bloom morph: stiffness 420, damping 30 */\n      --bcp-dur-bloom: 656ms;\n      --bcp-ease-bloom: linear(\n         0.0001,\n         0.0528,\n         0.1699,\n         0.311,\n         0.4611,\n         0.6007,\n         0.7213,\n         0.8196,\n         0.8936,\n         0.9496,\n         0.9882,\n         1.0127,\n         1.0264,\n         1.0323,\n         1.0331,\n         1.0307,\n         1.0264,\n         1.0215,\n         1.0167,\n         1.0122,\n         1.0083,\n         1.0053,\n         1.0029,\n         1.0013,\n         1.0001,\n         0.9994,\n         0.999,\n         0.9989,\n         0.9989,\n         0.999,\n         0.9992,\n         0.9993,\n         0.9995,\n         0.9996,\n         0.9998,\n         0.9998,\n         0.9999,\n         1,\n         1,\n         1,\n         1\n      );\n      /* dish: stiffness 420, damping 26 */\n      --bcp-dur-dish: 766ms;\n      --bcp-ease-dish: linear(\n         0.0001,\n         0.069,\n         0.226,\n         0.4115,\n         0.5942,\n         0.7535,\n         0.8825,\n         0.9726,\n         1.0307,\n         1.0623,\n         1.0743,\n         1.0724,\n         1.0627,\n         1.049,\n         1.035,\n         1.0223,\n         1.012,\n         1.0041,\n         0.999,\n         0.996,\n         0.9946,\n         0.9944,\n         0.995,\n         0.9959,\n         0.997,\n         0.998,\n         0.9989,\n         0.9995,\n         1,\n         1.0002,\n         1.0004,\n         1.0004,\n         1.0004,\n         1.0003,\n         1.0003,\n         1.0002,\n         1.0001,\n         1.0001,\n         1,\n         1,\n         1\n      );\n      /* arc: stiffness 300, damping 33 — near-critical, essentially no overshoot */\n      --bcp-dur-arc: 592ms;\n      --bcp-ease-arc: linear(\n         0.0001,\n         0.0315,\n         0.0995,\n         0.1898,\n         0.2845,\n         0.3821,\n         0.4742,\n         0.5555,\n         0.6303,\n         0.6934,\n         0.7495,\n         0.7969,\n         0.8353,\n         0.8682,\n         0.8944,\n         0.9166,\n         0.9345,\n         0.9484,\n         0.9599,\n         0.9688,\n         0.976,\n         0.9817,\n         0.986,\n         0.9895,\n         0.992,\n         0.9941,\n         0.9956,\n         0.9968,\n         0.9977,\n         0.9983,\n         0.9988,\n         0.9992,\n         0.9995,\n         0.9996,\n         0.9998,\n         0.9999,\n         0.9999,\n         1,\n         1,\n         1,\n         1\n      );\n      /* petal entrance: stiffness 220, damping 22 */\n      --bcp-dur-petal: 890ms;\n      --bcp-ease-petal: linear(\n         0.0001,\n         0.0502,\n         0.1608,\n         0.3026,\n         0.4465,\n         0.5844,\n         0.7021,\n         0.8014,\n         0.8777,\n         0.9359,\n         0.9762,\n         1.0034,\n         1.0194,\n         1.0277,\n         1.0303,\n         1.0293,\n         1.0263,\n         1.0221,\n         1.0177,\n         1.0134,\n         1.0097,\n         1.0066,\n         1.0041,\n         1.0023,\n         1.0009,\n         1,\n         0.9995,\n         0.9992,\n         0.9991,\n         0.9991,\n         0.9992,\n         0.9993,\n         0.9994,\n         0.9996,\n         0.9997,\n         0.9998,\n         0.9999,\n         0.9999,\n         1,\n         1,\n         1\n      );\n      /* petal hover: stiffness 400, damping 18 */\n      --bcp-dur-petal-hover: 1045ms;\n      --bcp-ease-petal-hover: linear(\n         0.0001,\n         0.1203,\n         0.3818,\n         0.6816,\n         0.9329,\n         1.1042,\n         1.1891,\n         1.2018,\n         1.1665,\n         1.1091,\n         1.0502,\n         1.0027,\n         0.972,\n         0.9593,\n         0.9599,\n         0.9688,\n         0.9812,\n         0.9928,\n         1.0016,\n         1.0067,\n         1.0085,\n         1.0078,\n         1.0057,\n         1.0032,\n         1.0009,\n         0.9993,\n         0.9985,\n         0.9983,\n         0.9985,\n         0.999,\n         0.9995,\n         0.9999,\n         1.0002,\n         1.0003,\n         1.0003,\n         1.0003,\n         1.0002,\n         1.0001,\n         1,\n         0.9999,\n         1\n      );\n      /* just a plain fast ease — no spring/overshoot for the default knob */\n      --bcp-dur-knob: 130ms;\n      --bcp-ease-knob: cubic-bezier(0.25, 0.1, 0.25, 1);\n      /* swatch press: stiffness 500, damping 24 */\n      --bcp-dur-press: 817ms;\n      --bcp-ease-press: linear(\n         0.0001,\n         0.0946,\n         0.3006,\n         0.5407,\n         0.7574,\n         0.9328,\n         1.0507,\n         1.1143,\n         1.1343,\n         1.1245,\n         1.0981,\n         1.0668,\n         1.0363,\n         1.0117,\n         0.9948,\n         0.9854,\n         0.982,\n         0.9829,\n         0.9862,\n         0.9904,\n         0.9946,\n         0.998,\n         1.0005,\n         1.0018,\n         1.0024,\n         1.0023,\n         1.0019,\n         1.0014,\n         1.0008,\n         1.0003,\n         1,\n         0.9998,\n         0.9997,\n         0.9997,\n         0.9997,\n         0.9998,\n         0.9999,\n         0.9999,\n         1,\n         1,\n         1\n      );\n   }\n}\n\n/* motion=\"bouncy\" — the same springs with lower damping, more overshoot.\n   Fallback cubic-bezier for browsers without linear() support first, then\n   the pre-computed linear() curves (stiffness/damping noted on each). */\n.bcp[data-motion=\"bouncy\"] {\n   --bcp-dur-bloom: 520ms;\n   --bcp-ease-bloom: cubic-bezier(0.3, 1.7, 0.4, 1);\n   --bcp-dur-dish: 560ms;\n   --bcp-ease-dish: cubic-bezier(0.3, 1.8, 0.4, 1);\n   --bcp-dur-arc: 620ms;\n   --bcp-ease-arc: cubic-bezier(0.3, 1.9, 0.4, 1);\n   --bcp-dur-petal-hover: 440ms;\n   --bcp-ease-petal-hover: cubic-bezier(0.3, 1.9, 0.4, 1);\n   --bcp-dur-knob: 400ms;\n   --bcp-ease-knob: cubic-bezier(0.3, 1.9, 0.4, 1);\n   --bcp-dur-press: 340ms;\n   --bcp-ease-press: cubic-bezier(0.3, 1.9, 0.4, 1);\n}\n\n@supports (transition-timing-function: linear(0, 1)) {\n   .bcp[data-motion=\"bouncy\"] {\n      /* bloom: stiffness 440, damping 24 */\n      --bcp-dur-bloom: 856ms;\n      --bcp-ease-bloom: linear(\n         0.0001,\n         0.0907,\n         0.2873,\n         0.5109,\n         0.7237,\n         0.8937,\n         1.0116,\n         1.0799,\n         1.1077,\n         1.108,\n         1.0912,\n         1.0669,\n         1.042,\n         1.021,\n         1.0049,\n         0.9945,\n         0.9892,\n         0.9877,\n         0.9888,\n         0.9911,\n         0.9939,\n         0.9965,\n         0.9986,\n         1.0001,\n         1.0009,\n         1.0013,\n         1.0013,\n         1.0011,\n         1.0008,\n         1.0005,\n         1.0003,\n         1.0001,\n         0.9999,\n         0.9999,\n         0.9998,\n         0.9999,\n         0.9999,\n         0.9999,\n         1,\n         1,\n         1\n      );\n      /* dish: stiffness 440, damping 21 */\n      --bcp-dur-dish: 975ms;\n      --bcp-ease-dish: linear(\n         0.0001,\n         0.1163,\n         0.3647,\n         0.6362,\n         0.8752,\n         1.043,\n         1.1343,\n         1.1615,\n         1.1456,\n         1.1066,\n         1.0618,\n         1.0232,\n         0.995,\n         0.9792,\n         0.974,\n         0.976,\n         0.9821,\n         0.9894,\n         0.9959,\n         1.0005,\n         1.0032,\n         1.0042,\n         1.0039,\n         1.003,\n         1.0018,\n         1.0008,\n         1,\n         0.9995,\n         0.9993,\n         0.9994,\n         0.9995,\n         0.9997,\n         0.9999,\n         1,\n         1.0001,\n         1.0001,\n         1.0001,\n         1.0001,\n         1.0001,\n         1,\n         1\n      );\n      /* arc: stiffness 320, damping 17 */\n      --bcp-dur-arc: 1156ms;\n      --bcp-ease-arc: linear(\n         0.0001,\n         0.1182,\n         0.3739,\n         0.6556,\n         0.902,\n         1.0727,\n         1.1615,\n         1.1821,\n         1.1573,\n         1.1093,\n         1.0573,\n         1.0137,\n         0.9846,\n         0.9697,\n         0.9669,\n         0.9721,\n         0.9809,\n         0.9904,\n         0.9981,\n         1.0032,\n         1.0057,\n         1.006,\n         1.0049,\n         1.0033,\n         1.0016,\n         1.0002,\n         0.9993,\n         0.9989,\n         0.9989,\n         0.9991,\n         0.9994,\n         0.9997,\n         1,\n         1.0001,\n         1.0002,\n         1.0002,\n         1.0002,\n         1.0001,\n         1,\n         1,\n         1\n      );\n      /* petal-hover: stiffness 420, damping 15 */\n      --bcp-dur-petal-hover: 1296ms;\n      --bcp-ease-petal-hover: linear(\n         0.0001,\n         0.1912,\n         0.5828,\n         0.9594,\n         1.208,\n         1.2898,\n         1.2416,\n         1.1319,\n         1.0191,\n         0.9434,\n         0.9162,\n         0.928,\n         0.9591,\n         0.9923,\n         1.0152,\n         1.0241,\n         1.0215,\n         1.0126,\n         1.0029,\n         0.9959,\n         0.9931,\n         0.9936,\n         0.9961,\n         0.999,\n         1.0011,\n         1.002,\n         1.0019,\n         1.0012,\n         1.0004,\n         0.9997,\n         0.9994,\n         0.9994,\n         0.9996,\n         0.9999,\n         1.0001,\n         1.0002,\n         1.0002,\n         1.0001,\n         1,\n         1,\n         1\n      );\n      /* knob: stiffness 420, damping 18 */\n      --bcp-dur-knob: 1129ms;\n      --bcp-ease-knob: linear(\n         0.0001,\n         0.1436,\n         0.4523,\n         0.7739,\n         1.0284,\n         1.1715,\n         1.2144,\n         1.1852,\n         1.1196,\n         1.0504,\n         0.9952,\n         0.9638,\n         0.9541,\n         0.96,\n         0.9736,\n         0.9888,\n         1.0006,\n         1.0076,\n         1.0098,\n         1.0087,\n         1.0057,\n         1.0025,\n         0.9999,\n         0.9984,\n         0.9979,\n         0.9981,\n         0.9987,\n         0.9994,\n         1,\n         1.0003,\n         1.0005,\n         1.0004,\n         1.0003,\n         1.0001,\n         1,\n         0.9999,\n         0.9999,\n         0.9999,\n         0.9999,\n         1,\n         1\n      );\n      /* press: stiffness 520, damping 19 */\n      --bcp-dur-press: 1041ms;\n      --bcp-ease-press: linear(\n         0.0001,\n         0.1541,\n         0.4765,\n         0.8143,\n         1.0701,\n         1.2069,\n         1.2342,\n         1.1875,\n         1.1084,\n         1.0309,\n         0.9746,\n         0.948,\n         0.9459,\n         0.9592,\n         0.9783,\n         0.9958,\n         1.0076,\n         1.0127,\n         1.0124,\n         1.0089,\n         1.0043,\n         1.0004,\n         0.9979,\n         0.9969,\n         0.9972,\n         0.9981,\n         0.9992,\n         1.0001,\n         1.0006,\n         1.0007,\n         1.0006,\n         1.0004,\n         1.0001,\n         1,\n         0.9999,\n         0.9998,\n         0.9999,\n         0.9999,\n         1,\n         1,\n         1\n      );\n   }\n}\n\n/* Swatch-sized anchor the bloom overlays so it opens where the swatch was */\n.bcp__slot {\n   position: relative;\n   width: calc(50px * var(--bcp-scale));\n   height: calc(50px * var(--bcp-scale));\n   flex-shrink: 0;\n}\n\n/* --- Closed swatch --- */\n.bcp__swatch {\n   width: calc(50px * var(--bcp-scale));\n   height: calc(50px * var(--bcp-scale));\n   border-radius: 50%;\n   border: none;\n   padding: 0;\n   cursor: pointer;\n   flex-shrink: 0;\n   box-shadow: 0 2px 8px var(--bcp-swatch-shadow);\n   outline: calc(1.5px * var(--bcp-scale)) solid var(--bcp-swatch-ring);\n   transition:\n      transform var(--bcp-dur-press) var(--bcp-ease-press),\n      background-color 0.25s ease-out;\n   animation: bcp-swatch-in 0.18s ease-out;\n}\n\n.bcp__swatch--pressing {\n   transform: scale(0.85);\n}\n\n.bcp__swatch:disabled {\n   cursor: not-allowed;\n   opacity: 0.55;\n}\n\n/* --- Hex input ---\n   The wrap is what's actually positioned/z-indexed (outside the swatch's\n   box, so it never shifts what the bloom considers \"center\" — the bloom\n   always opens from the swatch). It shrink-wraps to the input's real\n   rendered width (no JS measurement) since only `left` is set, not `right`.\n   Stays mounted (not hidden) while open. z-index sits below the arc(0),\n   bloom(1), and petals(2) so the bloom opens over it, not under it. */\n.bcp__input-wrap {\n   position: absolute;\n   z-index: -1;\n   top: 50%;\n   left: 0;\n   transform: translateY(-50%);\n   padding-left: calc(50px * var(--bcp-scale) + 14px * var(--bcp-scale));\n}\n\n.bcp__input {\n   border: 1px solid var(--bcp-input-border);\n   background: var(--bcp-input-bg);\n   border-radius: calc(16px * var(--bcp-scale));\n   padding: calc(10px * var(--bcp-scale)) calc(14px * var(--bcp-scale));\n   font-family: var(--bcp-font-input, ui-monospace, \"SF Mono\", \"Cascadia Code\", monospace);\n   font-size: calc(21px * var(--bcp-scale));\n   font-weight: 400;\n   color: var(--bcp-input-color);\n   transition: color 0.15s ease-out;\n   animation: bcp-swatch-in 0.18s ease-out;\n}\n\n.bcp__input:focus {\n   color: var(--bcp-input-color-focus);\n}\n\n.bcp__input:disabled {\n   cursor: not-allowed;\n   opacity: 0.55;\n}\n\n/* grouped: swatch + input share one pill background instead of two boxes.\n   The wrap's padding-left already reaches back under the swatch, so the\n   shared background just needs its own bg/border/height — the swatch\n   itself (a later, higher z-indexed sibling) paints over the left end. */\n.bcp[data-input-variant=\"grouped\"] .bcp__input-wrap {\n   display: flex;\n   align-items: center;\n   left: calc(-12px * var(--bcp-scale));\n   height: calc(70px * var(--bcp-scale));\n   padding-left: calc(50px * var(--bcp-scale) + 16px * var(--bcp-scale));\n   padding-right: calc(10px * var(--bcp-scale));\n   background: var(--bcp-input-bg);\n   border: 1px solid var(--bcp-input-border);\n   border-radius: calc(22px * var(--bcp-scale));\n}\n\n.bcp[data-input-variant=\"grouped\"] .bcp__input {\n   background: transparent;\n   border-color: transparent;\n   padding: calc(16px * var(--bcp-scale)) calc(6px * var(--bcp-scale));\n   font-size: calc(29px * var(--bcp-scale));\n}\n\n/* The input's own box sits smaller/offset inside the visible pill in grouped\n   mode, so a focus ring on the input itself doesn't trace the pill's actual\n   shape. Ring the wrap instead (its real border-radius) via :focus-within. */\n.bcp[data-input-variant=\"grouped\"] .bcp__input:focus-visible {\n   outline: none;\n}\n\n.bcp[data-input-variant=\"grouped\"] .bcp__input-wrap:focus-within {\n   outline: 2px solid var(--bcp-color-focus);\n   outline-offset: 2px;\n}\n\n/* bouncy: the swatch springs back in (scale overshoot) when the bloom\n   closes, reusing the press spring so it matches the swatch's own tap feel */\n.bcp[data-motion=\"bouncy\"] .bcp__swatch {\n   animation: bcp-swatch-in-bouncy var(--bcp-dur-press) var(--bcp-ease-press);\n}\n\n@keyframes bcp-swatch-in-bouncy {\n   from {\n      opacity: 0;\n      transform: scale(0.8);\n   }\n   to {\n      opacity: 1;\n      transform: scale(1);\n   }\n}\n\n@keyframes bcp-swatch-in {\n   from {\n      opacity: 0;\n   }\n   to {\n      opacity: 1;\n   }\n}\n\n/* --- Dahlia bloom --- */\n.bcp__bloom {\n   position: absolute;\n   z-index: 1;\n   top: 50%;\n   left: 50%;\n   width: calc(280px * var(--bcp-scale));\n   height: calc(280px * var(--bcp-scale));\n   margin: calc(-140px * var(--bcp-scale)) 0 0 calc(-140px * var(--bcp-scale));\n   border-radius: 50%;\n   box-sizing: border-box;\n   box-shadow: 0 8px 28px var(--bcp-bloom-shadow);\n   /* opens from swatch size (50/280) at the same center */\n   animation:\n      bcp-bloom-in var(--bcp-dur-bloom) var(--bcp-ease-bloom) backwards,\n      bcp-unblur-3 0.26s ease-out backwards;\n   transition: background-color 0.25s ease-out;\n}\n\n@keyframes bcp-bloom-in {\n   from {\n      transform: scale(0.1786);\n   }\n   to {\n      transform: scale(1);\n   }\n}\n\n@keyframes bcp-unblur-3 {\n   from {\n      filter: blur(var(--bcp-bloom-blur));\n   }\n   to {\n      filter: blur(0px);\n   }\n}\n\n.bcp__bloom--closing {\n   /* keyframe (not transition) so the exit reliably replaces the entrance animation */\n   animation: bcp-bloom-out 0.22s cubic-bezier(0.42, 0, 1, 1) forwards;\n}\n\n@keyframes bcp-bloom-out {\n   from {\n      transform: scale(1);\n      opacity: 1;\n      filter: blur(0px);\n   }\n   to {\n      transform: scale(0.1786);\n      opacity: 0;\n      filter: blur(var(--bcp-bloom-blur));\n   }\n}\n\n.bcp__dish {\n   position: absolute;\n   inset: calc(20px * var(--bcp-scale));\n   border-radius: 50%;\n   background: var(--bcp-dish-bg);\n   overflow: hidden;\n   animation: bcp-dish-in var(--bcp-dur-dish) var(--bcp-ease-dish) backwards;\n}\n\n/* The dish sits inside the bloom, so its scale multiplies with the bloom's own\n   0.1786 → 1. Starting from 0.4 meant an effective 0.07 on the first frame,\n   while the petals — siblings of the bloom, not children — animate at full size\n   from the start, so the dish read as arriving after them. It now starts close\n   to full and lets the parent's growth carry it. */\n@keyframes bcp-dish-in {\n   from {\n      transform: scale(0.86);\n      opacity: 0;\n      filter: blur(var(--bcp-dish-blur));\n   }\n   to {\n      transform: scale(1);\n      opacity: 1;\n      filter: blur(0px);\n   }\n}\n\n/* --- Brightness arc --- */\n.bcp__arc {\n   position: absolute;\n   top: 50%;\n   left: 50%;\n   margin-top: calc(-180px * var(--bcp-scale));\n   margin-left: calc(-180px * var(--bcp-scale));\n   transform-origin: center;\n   z-index: 0;\n   pointer-events: none;\n   overflow: visible;\n   animation: bcp-arc-in var(--bcp-dur-arc) var(--bcp-ease-arc) 0.28s backwards;\n}\n\n@keyframes bcp-arc-in {\n   from {\n      opacity: 0;\n      transform: scale(0.3);\n      filter: blur(var(--bcp-arc-blur));\n   }\n   to {\n      opacity: 1;\n      transform: scale(1);\n      filter: blur(0px);\n   }\n}\n\n.bcp__arc--closing {\n   animation: bcp-arc-out 0.16s cubic-bezier(0.42, 0, 1, 1) forwards;\n}\n\n@keyframes bcp-arc-out {\n   from {\n      opacity: 1;\n      transform: scale(1);\n      filter: blur(0px);\n   }\n   to {\n      opacity: 0;\n      transform: scale(0.3);\n      filter: blur(var(--bcp-arc-blur));\n   }\n}\n\n.bcp__knob {\n   pointer-events: auto;\n}\n\n.bcp__knob-halo {\n   r: 14.5px;\n   filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.25));\n   transition: r var(--bcp-dur-knob) var(--bcp-ease-knob);\n}\n\n.bcp__knob-core {\n   r: 11.5px;\n   transition: r var(--bcp-dur-knob) var(--bcp-ease-knob);\n}\n\n.bcp--dragging .bcp__knob-halo {\n   r: 16.5px;\n}\n\n.bcp--dragging .bcp__knob-core {\n   r: 13.5px;\n}\n\n/* --- Petals --- */\n.bcp__petal {\n   position: absolute;\n   z-index: 2;\n   border: none;\n   border-radius: 50%;\n   padding: 0;\n   cursor: pointer;\n   box-shadow: 0 1px 4px var(--bcp-petal-shadow);\n   /* spiral entrance from the center, staggered per petal */\n   animation: bcp-petal-in var(--bcp-dur-petal) var(--bcp-ease-petal) var(--bcp-petal-delay)\n      backwards;\n   transition: transform var(--bcp-dur-petal-hover) var(--bcp-ease-petal-hover);\n}\n\n@keyframes bcp-petal-in {\n   from {\n      transform: translate(var(--bcp-from-x), var(--bcp-from-y)) scale(1.5);\n      opacity: 0;\n      filter: blur(var(--bcp-petal-blur-in));\n   }\n   to {\n      transform: translate(0, 0) scale(1);\n      opacity: 1;\n      filter: blur(0px);\n   }\n}\n\n.bcp__petal--hovered {\n   transform: scale(1.18);\n}\n\n.bcp__petal:active {\n   transform: scale(0.95);\n   transition-duration: 0.1s;\n}\n\n/* close phase 2: all rings converge back to the center */\n.bcp__petal--home {\n   animation: bcp-petal-out 0.34s ease-in-out forwards;\n   transition: none;\n   pointer-events: none;\n}\n\n@keyframes bcp-petal-out {\n   from {\n      transform: translate(0, 0) scale(1);\n      opacity: 1;\n      filter: blur(0px);\n   }\n   to {\n      transform: translate(var(--bcp-from-x), var(--bcp-from-y)) scale(1);\n      opacity: 0;\n      filter: blur(var(--bcp-petal-blur-out));\n   }\n}\n\n/* motion=\"none\" is documented as an instant open/close — \"a quick uniform\n   fade, not an instant snap\" — but the override at the end of this file only\n   flattens durations, easings and delays; the keyframes still run the full\n   spiral. With the stagger gone, all 19 petals fly in from the centre on the\n   same frame inside 160ms, and the arc scales from 0.3 alongside them, which is\n   the most concentrated transform work this component can produce. These give\n   it the fade it actually promises — opacity only, so element-agnostic. */\n@keyframes bcp-fade-in {\n   from {\n      opacity: 0;\n   }\n   to {\n      opacity: 1;\n   }\n}\n\n@keyframes bcp-fade-out {\n   from {\n      opacity: 1;\n   }\n   to {\n      opacity: 0;\n   }\n}\n\n/* Touch devices drop the focus-pull entirely. Opening the bloom animates blur\n   on 22 layers at once — 19 petals, plus the bloom, the dish and the 360px arc,\n   which is an SVG being blurred while it scales from 0.3. Unlike the transform\n   and opacity beside them, blur() can't be composited: every frame re-rasterises\n   the layer and runs a Gaussian over it. Desktop GPUs absorb that; phone GPUs\n   drop frames on it.\n\n   Keyed on pointer type rather than width, so a narrowed desktop window keeps\n   the full effect and only the devices that actually struggle take the cheap\n   path. Everything still spirals, scales and fades; what goes is the decoration\n   that costs the most to paint and reads the least in motion — the focus-pull\n   here, plus the petals' shadow and ring below. */\n@media (hover: none) and (pointer: coarse) {\n   .bcp {\n      --bcp-petal-blur-in: 0px;\n      /* The close keeps a softened version rather than dropping to 0: it is a\n         quarter of the entrance radius over a fixed 340ms, and without any of\n         it the petals snap back to the centre instead of collapsing. Far\n         cheaper than the entrance, which is what actually caused the jank. */\n      --bcp-petal-blur-out: 1.5px;\n      --bcp-bloom-blur: 0px;\n      --bcp-dish-blur: 0px;\n      --bcp-arc-blur: 0px;\n   }\n\n   /* The rest of a petal's cost is paint, not motion: a blurred shadow and a\n      ring built from a nested color-mix(), on a 50%-radius box — repeated 19\n      times, all scaling at once. Both are decoration a phone can do without;\n      the petals stay legible on their own fills. The ring is set inline from\n      JS, so only !important can take it off — hence :not(:focus-visible), so\n      this can't also suppress the focus ring for anyone pairing a keyboard\n      with a touch device. */\n   .bcp__petal {\n      box-shadow: none;\n   }\n\n   .bcp__petal:not(:focus-visible) {\n      outline: none !important;\n   }\n\n   /* Scoped to touch, where the compressed motion reads as a jerk. Desktop\n      keeps its current motion=\"none\" look until that's a deliberate change.\n      The arc is the worst of them: a 360px SVG scaling from 0.3, so its paths\n      re-rasterise the whole way in. */\n   .bcp[data-motion=\"none\"] .bcp__petal,\n   .bcp[data-motion=\"none\"] .bcp__arc {\n      animation-name: bcp-fade-in;\n   }\n\n   .bcp[data-motion=\"none\"] .bcp__petal--home,\n   .bcp[data-motion=\"none\"] .bcp__arc--closing {\n      animation-name: bcp-fade-out;\n   }\n}\n\n@media (prefers-reduced-motion: reduce) {\n   .bcp,\n   .bcp * {\n      animation-duration: 0.01ms !important;\n      animation-delay: 0ms !important;\n      transition-duration: 0.01ms !important;\n      transition-delay: 0ms !important;\n   }\n}\n\n/* motion=\"none\" — a quick uniform fade, not an instant snap. Overrides every\n   spring/bounce curve and stagger delay with one plain ease-out, independent\n   of the visitor's OS motion setting. */\n.bcp[data-motion=\"none\"],\n.bcp[data-motion=\"none\"] * {\n   animation-duration: 160ms !important;\n   animation-timing-function: ease-out !important;\n   animation-delay: 0ms !important;\n   transition-duration: 160ms !important;\n   transition-timing-function: ease-out !important;\n   transition-delay: 0ms !important;\n}\n",
      "type": "registry:file",
      "target": "@components/bloom-color-picker/style.css"
    }
  ],
  "type": "registry:item"
}
