{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "gradient-picker-engine",
  "type": "registry:ui",
  "title": "Amplo Gradient Picker Engine",
  "description": "Dialect-agnostic core of the Amplo gradient picker: CSS gradient parsing/serialization with per-stop format state, the gradient context and hook, and the pointer-driven parts both variants share — Bar (with its stop-editor slot), Overlay, Area, StopList, StopColor, angle/position pads and inputs, ShapeSwitcher, ReverseStops, RepeatingToggle, Presets, CssInput, and the anchor-only stop popover shell. Installed automatically as a dependency of `gradient-picker` (Base UI) and `gradient-picker-radix`; you do not normally add it directly.",
  "version": "2.0.0",
  "categories": [
    "color",
    "input",
    "design",
    "accessibility"
  ],
  "dependencies": [
    "@base-ui/react",
    "lucide-react",
    "class-variance-authority"
  ],
  "registryDependencies": [
    "utils",
    "button",
    "toggle",
    "https://amplo.ale.design/r/color-picker-engine.json"
  ],
  "files": [
    {
      "path": "registry/new-york/ui/fill-picker/lib/gradient.ts",
      "type": "registry:lib",
      "target": "@ui/fill-picker/lib/gradient.ts",
      "content": "import type { OklchColor } from \"./types\";\nimport { formatColor, parseColor } from \"./color\";\n\nexport type GradientType = \"linear\" | \"radial\" | \"conic\";\n\nexport type GradientInterp =\n  | \"oklch\"\n  | \"oklab\"\n  | \"srgb\"\n  | \"hsl\"\n  | \"hsl-longer\";\n\nexport interface GradientStop {\n  color: OklchColor;\n  /** 0..1 along the gradient axis. */\n  position: number;\n  /** Optional CSS midpoint hint, 0..1. */\n  hint?: number;\n  /**\n   * Optional stable identity, honored in **controlled** mode only.\n   *\n   * Without it the picker reconciles an incoming `value` against its own\n   * state by position and array index, which cannot tell two stops sharing a\n   * position apart — reorder them externally and the colors swap between the\n   * existing internal ids while the selection and per-stop format stay put.\n   *\n   * Supply an id per stop and reconciliation matches on identity instead, so\n   * selection, per-stop color format, and colors all follow the right stop\n   * through any reorder. Ids must be unique within the gradient; duplicates\n   * are ignored and fall back to a generated id.\n   *\n   * Purely opt-in. Omit it and nothing changes — the picker reconciles by\n   * position exactly as before and never adds an `id` to what it emits.\n   *\n   * Once you do supply ids, they round-trip: `onValueChange` echoes them back\n   * (including on stops added inside the picker), so the ordinary\n   * `onValueChange={g => setG(g)}` pattern keeps identity instead of losing\n   * the tags on the first update.\n   */\n  id?: string;\n}\n\nexport interface LinearGradient {\n  type: \"linear\";\n  /**\n   * Degrees, 0..360. CSS gradient angle convention: 0deg = up, increases\n   * clockwise. Always present. When `start` and `end` are set, this field\n   * is kept in sync with `atan2(end - start)` so consumers reading `angle`\n   * alone still get a meaningful value.\n   */\n  angle: number;\n  /**\n   * When true, `formatGradient` emits `repeating-linear-gradient(...)`. The\n   * stop ramp repeats every (last - first) position span — typically used\n   * for stripes / barberpole patterns. Optional: undefined = false.\n   */\n  repeating?: boolean;\n  /**\n   * Optional start endpoint of the gradient line, normalized 0..1 of the\n   * gradient box in each axis. When both `start` and `end` are set, the\n   * gradient is treated as \"positioned\" — the line is the segment between\n   * them, and `formatGradient` emits CSS with stop positions adjusted so\n   * the visual transition happens between these two points (the CSS\n   * gradient line itself still passes through the box center because\n   * `linear-gradient` has no way to express an offset line, but the\n   * adjusted stop positions preserve the visual offset).\n   *\n   * When unset, the gradient behaves the legacy way: line passes through\n   * the box center, direction determined by `angle` only.\n   */\n  start?: { x: number; y: number };\n  /** Optional end endpoint. See `start`. */\n  end?: { x: number; y: number };\n  stops: GradientStop[];\n  interp: GradientInterp;\n}\n\n/**\n * CSS radial-gradient extent keywords — the four values the spec allows in\n * the `<size>` slot. They describe how far the gradient ellipse reaches\n * from its center, measured to either a side or a corner of the gradient\n * box. Spec: https://www.w3.org/TR/css-images-3/#valdef-radial-gradient-extent-keyword\n */\nexport type RadialSizeKeyword =\n  | \"closest-side\"\n  | \"closest-corner\"\n  | \"farthest-side\"\n  | \"farthest-corner\";\n\nexport interface RadialGradient {\n  type: \"radial\";\n  shape: \"circle\" | \"ellipse\";\n  /** Normalized 0..1 in each axis. */\n  center: { x: number; y: number };\n  size: RadialSizeKeyword;\n  /**\n   * Optional explicit radii. When set, takes precedence over `shape`+`size`\n   * for CSS emission. `x` is a fraction of the gradient box width, `y` a\n   * fraction of its height — matching the CSS `<length-percentage>{1,2}`\n   * radial-gradient ending-shape syntax. When unset, the keyword form\n   * (`shape size`) is emitted instead.\n   *\n   * Use this for **ellipse** shapes — pairs of percentages stay\n   * container-relative on the consumer side.\n   */\n  radii?: { x: number; y: number };\n  /**\n   * Optional explicit circle radius in absolute pixels. Only meaningful\n   * when `shape === \"circle\"`. When set, takes the highest precedence at\n   * emit time and produces a `radial-gradient(<px>px at ...)` form — which\n   * is the **only** way to get a CSS radial gradient that stays visually\n   * circular regardless of the consumer container's aspect ratio. The\n   * `<length-percentage>{2}` form always implies ellipse (CSS spec), so\n   * even rx === ry-by-pixel from the picker box draws an ellipse when\n   * pasted into a differently-shaped container.\n   */\n  radiusPx?: number;\n  stops: GradientStop[];\n  interp: GradientInterp;\n  /** See `LinearGradient.repeating`. Emits `repeating-radial-gradient(...)`. */\n  repeating?: boolean;\n}\n\nexport interface ConicGradient {\n  type: \"conic\";\n  /** Degrees. */\n  startAngle: number;\n  /** Normalized 0..1 in each axis. */\n  center: { x: number; y: number };\n  stops: GradientStop[];\n  interp: GradientInterp;\n  /** See `LinearGradient.repeating`. Emits `repeating-conic-gradient(...)`. */\n  repeating?: boolean;\n}\n\nexport type Gradient = LinearGradient | RadialGradient | ConicGradient;\n\nexport type ColorFill = { kind: \"color\"; color: OklchColor };\nexport type GradientFill = { kind: \"gradient\"; gradient: Gradient };\nexport type Fill = ColorFill | GradientFill;\n\nexport const DEFAULT_LINEAR: LinearGradient = {\n  type: \"linear\",\n  angle: 90,\n  interp: \"oklch\",\n  stops: [\n    { color: { l: 1, c: 0, h: 0, alpha: 1 }, position: 0 },\n    { color: { l: 0, c: 0, h: 0, alpha: 1 }, position: 1 },\n  ],\n};\n\nexport const DEFAULT_RADIAL: RadialGradient = {\n  type: \"radial\",\n  shape: \"circle\",\n  center: { x: 0.5, y: 0.5 },\n  size: \"farthest-corner\",\n  interp: \"oklch\",\n  stops: [\n    { color: { l: 1, c: 0, h: 0, alpha: 1 }, position: 0 },\n    { color: { l: 0, c: 0, h: 0, alpha: 1 }, position: 1 },\n  ],\n};\n\nexport const DEFAULT_CONIC: ConicGradient = {\n  type: \"conic\",\n  startAngle: 0,\n  center: { x: 0.5, y: 0.5 },\n  interp: \"oklch\",\n  stops: [\n    { color: { l: 1, c: 0, h: 0, alpha: 1 }, position: 0 },\n    { color: { l: 0, c: 0, h: 0, alpha: 1 }, position: 1 },\n  ],\n};\n\nfunction trim(n: number, digits = 4): string {\n  return Number(n.toFixed(digits)).toString();\n}\n\nconst formatStopColor = (c: OklchColor): string => formatColor(c, \"oklch\");\n\nfunction formatStops(stops: GradientStop[]): string {\n  const parts: string[] = [];\n  stops.forEach((s, i) => {\n    // A hint on stop i is the midpoint *between* stop i-1 and stop i —\n    // emit it before this stop's color declaration.\n    if (s.hint !== undefined && i > 0) {\n      parts.push(`${trim(s.hint * 100)}%`);\n    }\n    parts.push(`${formatStopColor(s.color)} ${trim(s.position * 100)}%`);\n  });\n  return parts.join(\", \");\n}\n\nfunction formatInterp(interp: GradientInterp): string {\n  if (interp === \"hsl-longer\") return \"hsl longer hue\";\n  return interp;\n}\n\n/**\n * Derive a CSS gradient angle (0deg = up, increases clockwise) from two\n * box-normalized points. Returns undefined when either point is missing or\n * the two coincide.\n */\nexport function angleFromPoints(\n  start: { x: number; y: number } | undefined,\n  end: { x: number; y: number } | undefined,\n): number | undefined {\n  if (!start || !end) return undefined;\n  const dx = end.x - start.x;\n  // Box y axis points down in screen coords, but CSS gradient angle is\n  // measured with 0deg = up. Flip dy so the formula matches the visual.\n  const dy = -(end.y - start.y);\n  if (dx === 0 && dy === 0) return undefined;\n  // atan2(x, y) with CSS convention: angle = arctan2(dx, dy) gives 0 = up,\n  // increases clockwise. Same convention used by AnglePad and Area.\n  const deg = (Math.atan2(dx, dy) * 180) / Math.PI;\n  return ((deg % 360) + 360) % 360;\n}\n\n/**\n * Project a linear gradient's `start` and `end` endpoints onto the CSS\n * gradient line (the centered line implied by the derived angle). Returns\n * `null` when the segment has zero length and no projection is meaningful.\n *\n * The CSS gradient line length depends on the actual box dimensions; this\n * projection assumes a square unit box, so the magnitudes of the returned\n * `startProj` / `endProj` are an approximation in stretched containers.\n * The direction is always correct.\n */\nexport function gradientLineProjection(\n  start: { x: number; y: number },\n  end: { x: number; y: number },\n): { startProj: number; endProj: number } | null {\n  const dx = end.x - start.x;\n  const dy = end.y - start.y;\n  const len = Math.hypot(dx, dy);\n  if (len === 0) return null;\n  const ux = dx / len;\n  const uy = dy / len;\n  const cssLen = Math.abs(ux) + Math.abs(uy);\n  const project = (p: { x: number; y: number }) =>\n    0.5 + ((p.x - 0.5) * ux + (p.y - 0.5) * uy) / cssLen;\n  return { startProj: project(start), endProj: project(end) };\n}\n\n/**\n * Map an authored stop position (0..1 along the [start, end] segment) into\n * its visible position along the CSS gradient line. Identity when the\n * segment has zero length or either endpoint is missing.\n */\nexport function projectStopPosition(\n  authoredPos: number,\n  start: { x: number; y: number } | undefined,\n  end: { x: number; y: number } | undefined,\n): number {\n  if (!start || !end) return authoredPos;\n  const proj = gradientLineProjection(start, end);\n  if (!proj) return authoredPos;\n  return proj.startProj + (proj.endProj - proj.startProj) * authoredPos;\n}\n\n/**\n * Inverse of `projectStopPosition`: take a position along the CSS gradient\n * line and return the corresponding authored position. Useful for\n * converting a pointer position on the Bar back into the value that should\n * be written to `stop.position`. Returns the input unchanged when there's\n * no segment to project onto.\n */\nexport function reverseProjectStopPosition(\n  displayedPos: number,\n  start: { x: number; y: number } | undefined,\n  end: { x: number; y: number } | undefined,\n): number {\n  if (!start || !end) return displayedPos;\n  const proj = gradientLineProjection(start, end);\n  if (!proj) return displayedPos;\n  const span = proj.endProj - proj.startProj;\n  if (span === 0) return 0;\n  return (displayedPos - proj.startProj) / span;\n}\n\n/**\n * Re-map a list of stop positions so that the gradient transition happens\n * between `start` and `end` along the centered CSS gradient line. Used by\n * `formatGradient` for the positioned linear case, and by the Bar /\n * StopList parts to mirror that projection in their UI.\n */\nexport function adjustStopsForEndpoints(\n  stops: GradientStop[],\n  start: { x: number; y: number },\n  end: { x: number; y: number },\n): GradientStop[] {\n  const proj = gradientLineProjection(start, end);\n  if (!proj) return stops;\n  const { startProj, endProj } = proj;\n  return stops.map((s) => ({\n    ...s,\n    position: startProj + (endProj - startProj) * s.position,\n    hint:\n      s.hint === undefined\n        ? undefined\n        : startProj + (endProj - startProj) * s.hint,\n  }));\n}\n\nexport function formatGradient(g: Gradient): string {\n  const interp = `in ${formatInterp(g.interp)}`;\n  const prefix = g.repeating ? \"repeating-\" : \"\";\n  if (g.type === \"linear\") {\n    if (g.start && g.end) {\n      const derivedAngle = angleFromPoints(g.start, g.end);\n      // If start and end coincide (zero-length line), there's no meaningful\n      // direction — fall through to the plain angle form.\n      if (derivedAngle !== undefined) {\n        const adjusted = adjustStopsForEndpoints(g.stops, g.start, g.end);\n        return `${prefix}linear-gradient(${interp} ${trim(derivedAngle)}deg, ${formatStops(adjusted)})`;\n      }\n    }\n    return `${prefix}linear-gradient(${interp} ${trim(g.angle)}deg, ${formatStops(g.stops)})`;\n  }\n  if (g.type === \"radial\") {\n    const center = `at ${trim(g.center.x * 100)}% ${trim(g.center.y * 100)}%`;\n    // Precedence at emit time:\n    //   1. shape=circle + radiusPx → `<px>px` (single length forces circle,\n    //      stays a true circle in any consumer container)\n    //   2. radii → `<rx>% <ry>%` (always renders as an ellipse, scales with\n    //      container width / height respectively)\n    //   3. keyword form → `shape size` (CSS default, computed live by the\n    //      renderer relative to the container + center)\n    let endingShape: string;\n    if (g.shape === \"circle\" && g.radiusPx !== undefined) {\n      endingShape = `${trim(g.radiusPx)}px`;\n    } else if (g.shape === \"ellipse\" && g.radii) {\n      // The `% %` pair always implies ellipse in CSS, so it must never be\n      // emitted for shape: \"circle\" — stale radii from an earlier ellipse\n      // edit would silently flip the rendered shape.\n      endingShape = `${trim(g.radii.x * 100)}% ${trim(g.radii.y * 100)}%`;\n    } else {\n      endingShape = `${g.shape} ${g.size}`;\n    }\n    return `${prefix}radial-gradient(${endingShape} ${center} ${interp}, ${formatStops(g.stops)})`;\n  }\n  // conic\n  const center = `at ${trim(g.center.x * 100)}% ${trim(g.center.y * 100)}%`;\n  return `${prefix}conic-gradient(from ${trim(g.startAngle)}deg ${center} ${interp}, ${formatStops(g.stops)})`;\n}\n\n// ---------------------------------------------------------------------------\n// parseGradient — inverse of formatGradient\n// ---------------------------------------------------------------------------\n\nconst FN_RE = /^(repeating-)?(linear|radial|conic)-gradient\\((.*)\\)$/is;\n\n/** Split on commas that are not nested inside parentheses. */\nfunction splitTopLevel(input: string): string[] {\n  const out: string[] = [];\n  let depth = 0;\n  let buf = \"\";\n  for (const ch of input) {\n    if (ch === \"(\") depth++;\n    else if (ch === \")\") depth--;\n    if (ch === \",\" && depth === 0) {\n      out.push(buf.trim());\n      buf = \"\";\n    } else {\n      buf += ch;\n    }\n  }\n  if (buf.trim()) out.push(buf.trim());\n  return out;\n}\n\n/**\n * Extract `in <space>[ longer hue]` from anywhere within a string.\n * Returns the parsed interp and the string with the `in …` clause removed.\n */\nfunction extractInterp(s: string): { interp: GradientInterp; rest: string } {\n  // `in <space> [<hue-method> hue]` — only `hsl longer hue` has a model\n  // value; other hue methods are accepted and dropped rather than left\n  // in `rest` to poison the header parse.\n  const m = s.match(\n    /\\bin\\s+([a-z0-9-]+)(?:\\s+(shorter|longer|increasing|decreasing)\\s+hue)?\\b/i,\n  );\n  if (!m) return { interp: \"oklch\", rest: s };\n  const space = m[1].toLowerCase();\n  const longer = m[2]?.toLowerCase() === \"longer\";\n  let interp: GradientInterp = \"oklch\";\n  if (space === \"hsl\") interp = longer ? \"hsl-longer\" : \"hsl\";\n  else if (space === \"oklab\") interp = \"oklab\";\n  else if (space === \"srgb\") interp = \"srgb\";\n  return { interp, rest: s.replace(m[0], \"\").trim() };\n}\n\nfunction parseStops(parts: string[]): GradientStop[] | null {\n  const raw: Array<{\n    color: OklchColor;\n    position: number | null;\n    hint?: number;\n  }> = [];\n  // A hint belongs to the stop that FOLLOWS it (formatStops emits stop i's\n  // hint between stop i-1 and stop i), so stash it until the next color stop.\n  let pendingHint: number | undefined;\n  for (const p of parts) {\n    // Bare percentage hint: `30%`\n    const hintMatch = p.match(/^(-?\\d+(?:\\.\\d+)?)%$/);\n    if (hintMatch) {\n      if (raw.length === 0) return null; // hint can't lead\n      if (pendingHint !== undefined) return null; // two hints in a row\n      pendingHint = parseFloat(hintMatch[1]) / 100;\n      continue;\n    }\n    // Color + up to two positions: `oklch(…)`, `oklch(…) 50%`, or the\n    // hard-stop shorthand `red 0% 50%`, which is two stops of one color.\n    const two = p.match(/^(.*?)\\s+(-?\\d+(?:\\.\\d+)?)%\\s+(-?\\d+(?:\\.\\d+)?)%$/);\n    const one = two ? null : p.match(/^(.*?)\\s+(-?\\d+(?:\\.\\d+)?)%$/);\n    const colorStr = (two ?? one)?.[1].trim() ?? p;\n    const positions: Array<number | null> = two\n      ? [parseFloat(two[2]) / 100, parseFloat(two[3]) / 100]\n      : [one ? parseFloat(one[2]) / 100 : null];\n    const color = parseColor(colorStr);\n    if (!color) return null;\n    positions.forEach((position, k) => {\n      raw.push({\n        color: k === 0 ? color : { ...color },\n        position,\n        ...(k === 0 && pendingHint !== undefined ? { hint: pendingHint } : {}),\n      });\n    });\n    pendingHint = undefined;\n  }\n  if (pendingHint !== undefined) return null; // hint can't trail\n  if (raw.length === 0) return null;\n\n  // CSS Images 3 §3.4.3 position fixup:\n  //  1. First/last stops without a position default to 0% / 100%.\n  //  2. Explicit positions may never decrease — clamp to the running max.\n  //  3. Runs of unpositioned stops are spaced evenly between their\n  //     positioned neighbors.\n  if (raw[0].position === null) raw[0].position = 0;\n  if (raw[raw.length - 1].position === null) raw[raw.length - 1].position = 1;\n  let runningMax = raw[0].position as number;\n  for (const s of raw) {\n    if (s.position !== null) {\n      s.position = Math.max(s.position, runningMax);\n      runningMax = s.position;\n    }\n  }\n  let i = 0;\n  while (i < raw.length) {\n    if (raw[i].position !== null) {\n      i++;\n      continue;\n    }\n    let j = i;\n    while (raw[j].position === null) j++;\n    const prev = raw[i - 1].position as number;\n    const next = raw[j].position as number;\n    const count = j - i + 1; // gaps between prev and next\n    for (let k = i; k < j; k++) {\n      raw[k].position = prev + ((next - prev) * (k - i + 1)) / count;\n    }\n    i = j;\n  }\n  return raw.map((s) => ({\n    color: s.color,\n    position: s.position as number,\n    ...(s.hint !== undefined ? { hint: s.hint } : {}),\n  }));\n}\n\n/**\n * Map a CSS `to <side-or-corner>` linear direction to a gradient angle.\n * Corners use the square-box angles (45/135/225/315) — the picker's model\n * only stores an angle, so the CSS \"magic corner\" aspect-dependence is\n * intentionally approximated. Returns null for invalid pairs\n * (`to left right`).\n */\nfunction sideOrCornerAngle(\n  a: string,\n  b: string | undefined,\n): number | null {\n  const set = new Set([a.toLowerCase(), ...(b ? [b.toLowerCase()] : [])]);\n  if (b && set.size !== 2) return null;\n  const has = (s: string) => set.has(s);\n  if ((has(\"top\") && has(\"bottom\")) || (has(\"left\") && has(\"right\"))) {\n    return null;\n  }\n  if (set.size === 1) {\n    if (has(\"top\")) return 0;\n    if (has(\"right\")) return 90;\n    if (has(\"bottom\")) return 180;\n    return 270; // left\n  }\n  if (has(\"top\") && has(\"right\")) return 45;\n  if (has(\"bottom\") && has(\"right\")) return 135;\n  if (has(\"bottom\") && has(\"left\")) return 225;\n  return 315; // top left\n}\n\nconst NUM_RE_SRC = String.raw`-?\\d+(?:\\.\\d+)?`;\nconst PCT_RE = new RegExp(`^(${NUM_RE_SRC})%$`);\nconst PX_RE = new RegExp(`^(${NUM_RE_SRC})px$`, \"i\");\nconst ANGLE_RE = new RegExp(`^(${NUM_RE_SRC})(deg|turn|rad|grad)?$`, \"i\");\nconst RADIAL_SIZES: readonly string[] = [\n  \"closest-side\",\n  \"closest-corner\",\n  \"farthest-side\",\n  \"farthest-corner\",\n] satisfies RadialSizeKeyword[];\n\nconst wrapDeg = (d: number) => ((d % 360) + 360) % 360;\n\n/** CSS `<angle>` → degrees in [0, 360). Unitless is accepted only for `0`. */\nfunction parseAngle(s: string): number | null {\n  const m = s.trim().match(ANGLE_RE);\n  if (!m) return null;\n  const n = parseFloat(m[1]);\n  switch (m[2]?.toLowerCase()) {\n    case \"deg\":\n      return wrapDeg(n);\n    case \"turn\":\n      return wrapDeg(n * 360);\n    case \"rad\":\n      return wrapDeg((n * 180) / Math.PI);\n    case \"grad\":\n      return wrapDeg(n * 0.9);\n    default:\n      return n === 0 ? 0 : null;\n  }\n}\n\n/**\n * CSS `<position>` (one or two tokens: percentages and side keywords) →\n * 0..1 fractions. Pixel offsets aren't representable in the model → null.\n */\nfunction parsePosition(s: string): { x: number; y: number } | null {\n  const toks = s.trim().toLowerCase().split(/\\s+/).filter(Boolean);\n  const X: Record<string, number> = { left: 0, center: 0.5, right: 1 };\n  const Y: Record<string, number> = { top: 0, center: 0.5, bottom: 1 };\n  const pct = (t: string) => {\n    const m = t.match(PCT_RE);\n    return m ? parseFloat(m[1]) / 100 : undefined;\n  };\n  if (toks.length === 1) {\n    const [t] = toks;\n    const p = pct(t);\n    if (p !== undefined) return { x: p, y: 0.5 };\n    if (t in X) return { x: X[t], y: 0.5 };\n    if (t in Y) return { x: 0.5, y: Y[t] };\n    return null;\n  }\n  if (toks.length !== 2) return null;\n  let [a, b] = toks;\n  // Keyword pairs may come vertical-first (`top right`).\n  if ((a in Y && !(a in X)) || (b in X && !(b in Y))) [a, b] = [b, a];\n  const x = pct(a) ?? X[a];\n  const y = pct(b) ?? Y[b];\n  return x === undefined || y === undefined ? null : { x, y };\n}\n\ninterface RadialHeader {\n  shape: \"circle\" | \"ellipse\";\n  size: RadialSizeKeyword;\n  center: { x: number; y: number };\n  radii?: { x: number; y: number };\n  radiusPx?: number;\n}\n\n/**\n * Parse a radial header (`circle farthest-corner at 50% 50%`, `48% 30%`,\n * `268px at top`, …). Returns null on any unrecognized token so invalid\n * input is rejected instead of silently becoming the default gradient.\n */\nfunction parseRadialHeader(head: string): RadialHeader | null {\n  const [beforeAt, afterAt, ...extra] = head.split(/\\bat\\b/i);\n  if (extra.length > 0) return null;\n  let center = { x: 0.5, y: 0.5 };\n  if (afterAt !== undefined) {\n    const pos = parsePosition(afterAt);\n    if (!pos) return null;\n    center = pos;\n  }\n  let shape: \"circle\" | \"ellipse\" | undefined;\n  let size: RadialSizeKeyword | undefined;\n  const lengths: string[] = [];\n  for (const tok of beforeAt.trim().toLowerCase().split(/\\s+/).filter(Boolean)) {\n    if (tok === \"circle\" || tok === \"ellipse\") {\n      if (shape) return null;\n      shape = tok;\n    } else if (RADIAL_SIZES.includes(tok)) {\n      if (size) return null;\n      size = tok as RadialSizeKeyword;\n    } else if (PX_RE.test(tok) || PCT_RE.test(tok)) {\n      lengths.push(tok);\n    } else {\n      return null;\n    }\n  }\n  if (lengths.length > 2) return null;\n  const header: RadialHeader = {\n    shape: shape ?? \"ellipse\",\n    size: size ?? \"farthest-corner\",\n    center,\n  };\n  if (lengths.length === 1) {\n    // A single `<length>` is only valid as a circle radius; a lone\n    // percentage isn't valid CSS.\n    const px = lengths[0].match(PX_RE);\n    if (!px) return null;\n    header.radiusPx = parseFloat(px[1]);\n    header.shape = \"circle\";\n  } else if (lengths.length === 2) {\n    // Only the percentage pair formatGradient emits maps onto the model;\n    // raw lengths (`100px 80px`) fall through to the keyword defaults\n    // rather than being silently rescaled.\n    const [rx, ry] = lengths.map((t) => t.match(PCT_RE));\n    if (rx && ry) {\n      header.radii = { x: parseFloat(rx[1]) / 100, y: parseFloat(ry[1]) / 100 };\n    }\n  }\n  return header;\n}\n\ninterface ConicHeader {\n  startAngle: number;\n  center: { x: number; y: number };\n}\n\n/** Parse a conic header (`from 90deg at 50% 50%`). Null on junk. */\nfunction parseConicHeader(head: string): ConicHeader | null {\n  const [beforeAt, afterAt, ...extra] = head.split(/\\bat\\b/i);\n  if (extra.length > 0) return null;\n  let center = { x: 0.5, y: 0.5 };\n  if (afterAt !== undefined) {\n    const pos = parsePosition(afterAt);\n    if (!pos) return null;\n    center = pos;\n  }\n  let startAngle = 0;\n  const from = beforeAt.trim();\n  if (from) {\n    const m = from.match(/^from\\s+(\\S+)$/i);\n    const angle = m ? parseAngle(m[1]) : null;\n    if (angle === null) return null;\n    startAngle = angle;\n  }\n  return { startAngle, center };\n}\n\nexport function parseGradient(input: string): Gradient | null {\n  const trimmed = input.trim();\n  const m = trimmed.match(FN_RE);\n  if (!m) return null;\n\n  const repeating = !!m[1];\n  const type = m[2].toLowerCase() as GradientType;\n  const parts = splitTopLevel(m[3]);\n  if (parts.length === 0) return null;\n\n  if (type === \"linear\") {\n    // parts[0] for formatGradient output: \"in oklch 90deg\"\n    // For a hand-written gradient without interp: \"90deg\" or absent (just stops)\n    const { interp, rest } = extractInterp(parts[0]);\n    let angle = 180; // CSS default\n    let stopParts = parts.slice(1);\n\n    const parsedAngle = parseAngle(rest);\n    const toMatch = rest.match(\n      /^to\\s+(top|bottom|left|right)(?:\\s+(top|bottom|left|right))?$/i,\n    );\n    if (parsedAngle !== null) {\n      angle = parsedAngle;\n    } else if (toMatch) {\n      const dir = sideOrCornerAngle(toMatch[1], toMatch[2]);\n      if (dir === null) return null; // e.g. \"to left right\"\n      angle = dir;\n    } else if (rest.length > 0) {\n      // rest wasn't a direction — treat it as the first stop\n      stopParts = [rest, ...stopParts];\n    }\n\n    const stops = parseStops(stopParts);\n    if (!stops) return null;\n    return {\n      type: \"linear\",\n      angle,\n      interp,\n      stops,\n      ...(repeating ? { repeating: true } : {}),\n    };\n  }\n\n  // radial / conic: a header-less gradient (`radial-gradient(red, blue)`)\n  // starts straight with its stops, so when parts[0] isn't a valid header\n  // and carried no `in <space>` clause, re-read it as the first stop.\n  const { interp, rest } = extractInterp(parts[0]);\n  const hadInterp = rest !== parts[0];\n  const header =\n    type === \"radial\" ? parseRadialHeader(rest) : parseConicHeader(rest);\n  if (!header && hadInterp) return null;\n  const stops = parseStops(header ? parts.slice(1) : parts);\n  if (!stops) return null;\n\n  if (type === \"radial\") {\n    const h = (header as RadialHeader | null) ?? {\n      shape: \"ellipse\",\n      size: \"farthest-corner\",\n      center: { x: 0.5, y: 0.5 },\n    };\n    return {\n      type: \"radial\",\n      shape: h.shape,\n      size: h.size,\n      center: h.center,\n      ...(h.radii ? { radii: h.radii } : {}),\n      ...(h.radiusPx !== undefined ? { radiusPx: h.radiusPx } : {}),\n      interp,\n      stops,\n      ...(repeating ? { repeating: true } : {}),\n    };\n  }\n\n  const h = (header as ConicHeader | null) ?? {\n    startAngle: 0,\n    center: { x: 0.5, y: 0.5 },\n  };\n  return {\n    type: \"conic\",\n    startAngle: h.startAngle,\n    center: h.center,\n    interp,\n    stops,\n    ...(repeating ? { repeating: true } : {}),\n  };\n}\n\n// ---------------------------------------------------------------------------\n// parseFill / formatFill — discriminated union of Color | Gradient\n// ---------------------------------------------------------------------------\n\nexport function formatFill(f: Fill): string {\n  if (f.kind === \"color\") return formatColor(f.color, \"oklch\");\n  return formatGradient(f.gradient);\n}\n\nexport function parseFill(input: string): Fill | null {\n  const g = parseGradient(input);\n  if (g) return { kind: \"gradient\", gradient: g };\n  const c = parseColor(input);\n  if (c) return { kind: \"color\", color: c };\n  return null;\n}\n\n// ---------------------------------------------------------------------------\n// sampleStopsAt — interpolate a stop color at an arbitrary 0..1 position\n// ---------------------------------------------------------------------------\n\nconst lerp = (a: number, b: number, t: number) => a + (b - a) * t;\n\n// Shortest-path hue lerp on a 0..360 circle. Matches what users perceive in\n// an OKLCH-rendered gradient between two adjacent stops.\nconst ACHROMATIC_C = 1e-4;\n\nfunction lerpHue(a: number, b: number, t: number): number {\n  let d = b - a;\n  if (d > 180) d -= 360;\n  else if (d < -180) d += 360;\n  return ((a + d * t) % 360 + 360) % 360;\n}\n\n/**\n * Returns the OKLCH color at `position` (0..1) along a sorted-by-position\n * stop list. Used by Bar so click-to-add picks the color the user sees.\n * Interpolation is done in OKLCH regardless of the gradient's `interp`\n * setting — the stops themselves are canonical OKLCH and that gives a\n * perceptually sensible pick even when the visual paint uses sRGB/HSL.\n */\nexport function sampleStopsAt(\n  stops: GradientStop[],\n  position: number,\n): OklchColor {\n  if (stops.length === 0) return { l: 0.5, c: 0, h: 0, alpha: 1 };\n  const sorted = [...stops].sort((a, b) => a.position - b.position);\n  if (position <= sorted[0].position) return { ...sorted[0].color };\n  const last = sorted[sorted.length - 1];\n  if (position >= last.position) return { ...last.color };\n  let i = 0;\n  while (i < sorted.length - 1 && sorted[i + 1].position < position) i++;\n  const a = sorted[i];\n  const b = sorted[i + 1];\n  const span = b.position - a.position;\n  let t = span === 0 ? 0 : (position - a.position) / span;\n  // CSS midpoint hint: the hint (stored on the following stop, in absolute\n  // axis coordinates like `position`) marks where the blend reaches 50%.\n  // Browsers warp progress with t^(log(.5)/log(h)) so t = h lands at 0.5 —\n  // apply the same curve so click-to-add picks the color the user sees.\n  if (b.hint !== undefined && span > 0) {\n    const h = (b.hint - a.position) / span;\n    if (h > 0 && h < 1) {\n      t = Math.pow(t, Math.log(0.5) / Math.log(h));\n    }\n  }\n  // An achromatic stop's hue is powerless; CSS Color 4 treats it as missing\n  // and borrows the other endpoint's hue instead of sweeping through 0°.\n  const ha = a.color.c <= ACHROMATIC_C ? b.color.h : a.color.h;\n  const hb = b.color.c <= ACHROMATIC_C ? a.color.h : b.color.h;\n  return {\n    l: lerp(a.color.l, b.color.l, t),\n    c: lerp(a.color.c, b.color.c, t),\n    h: lerpHue(ha, hb, t),\n    alpha: lerp(a.color.alpha, b.color.alpha, t),\n  };\n}\n"
    },
    {
      "path": "registry/new-york/ui/fill-picker/lib/gradient-options.ts",
      "type": "registry:lib",
      "target": "@ui/fill-picker/lib/gradient-options.ts",
      "content": "import type {\n  GradientInterp,\n  GradientType,\n  RadialSizeKeyword,\n} from \"./gradient\";\n\n/**\n * Option metadata shared by both switcher variants (classic and Base UI).\n * Living here — not copy-pasted into each tree — means a label or tooltip\n * edit can't desync the two product surfaces.\n */\n\nexport const GRADIENT_TYPE_OPTIONS: {\n  value: GradientType;\n  label: string;\n}[] = [\n  { value: \"linear\", label: \"Linear\" },\n  { value: \"radial\", label: \"Radial\" },\n  { value: \"conic\", label: \"Conic\" },\n];\n\n/**\n * Selectable interpolation spaces. Each maps to the CSS Color 4\n * `<gradient> in <space>` clause emitted by `formatGradient`. The\n * description is surfaced via the ⓘ icon next to each option, so users\n * hovering an option in the open menu see *why* they'd pick it.\n */\nexport const GRADIENT_INTERP_OPTIONS: {\n  value: GradientInterp;\n  label: string;\n  description: string;\n}[] = [\n  {\n    value: \"oklch\",\n    label: \"OKLCH\",\n    description:\n      \"Perceptually uniform polar space. Smooth hue arcs, no muddy mid-tones. Recommended default.\",\n  },\n  {\n    value: \"oklab\",\n    label: \"OKLab\",\n    description:\n      \"Perceptual but cartesian — a straight line through OK space. Smooth lightness, no hue rotation.\",\n  },\n  {\n    value: \"srgb\",\n    label: \"sRGB\",\n    description:\n      \"Legacy browser default. Mixes in gamma-encoded sRGB; often grays through the middle of two saturated colors.\",\n  },\n  {\n    value: \"hsl\",\n    label: \"HSL\",\n    description:\n      \"Walks the hue circle the shorter way between the two stops.\",\n  },\n  {\n    value: \"hsl-longer\",\n    label: \"HSL (longer hue)\",\n    description:\n      \"Walks the hue circle the longer way — produces a full rainbow sweep between two stops.\",\n  },\n];\n\n/**\n * The four CSS `<size>` keywords for radial gradients, each with a\n * one-sentence hover explanation surfaced via the ⓘ icon.\n *\n * https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/radial-gradient#size\n */\nexport const RADIAL_SIZE_OPTIONS: {\n  value: RadialSizeKeyword;\n  description: string;\n}[] = [\n  {\n    value: \"closest-side\",\n    description:\n      \"Ends at the side of the box closest to the center (the shortest reachable edge).\",\n  },\n  {\n    value: \"closest-corner\",\n    description:\n      \"Ends at the corner of the box closest to the center — passes through the nearest corner.\",\n  },\n  {\n    value: \"farthest-side\",\n    description:\n      \"Ends at the side of the box farthest from the center (the longest reachable edge).\",\n  },\n  {\n    value: \"farthest-corner\",\n    description:\n      \"Default. Ends at the corner of the box farthest from the center — gradient covers the entire box.\",\n  },\n];\n"
    },
    {
      "path": "registry/new-york/ui/fill-picker/contexts/gradient.ts",
      "type": "registry:lib",
      "target": "@ui/fill-picker/contexts/gradient.ts",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport type { GradientPickerState } from \"../hooks/use-gradient-picker\";\n\nexport const GradientPickerContext =\n  React.createContext<GradientPickerState | null>(null);\n\n/**\n * Renders the per-stop color editor for `<GradientPicker.Bar editOnClick>`.\n *\n * The Bar owns the *interaction* (which stop is being edited, and when a tap\n * counts as an edit) but not the editor's UI dialect — that ships per variant\n * so the shared Bar can live in a dialect-clean engine item. Each barrel\n * injects its own default through `<Root stopEditor>`; consumers can override\n * it with the same prop.\n *\n * `children` is the stop handle: the renderer must render it (that's the\n * element the popover anchors to and the element the user drags).\n *\n * Called as a plain function inside the parts' stop `.map()`, not mounted as\n * JSX — so it must *return* a component element and never call hooks itself,\n * or the hook order breaks the moment a stop is added or removed.\n */\nexport type GradientStopEditorRenderer = (props: {\n  stopId: string;\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  /** The stop handle element to anchor/wrap. */\n  children: React.ReactNode;\n}) => React.ReactNode;\n\n/**\n * Carried separately from `GradientPickerContext` on purpose: the gradient\n * state is also provided by `<FillPicker.Pane mode=\"gradient\">`, which builds\n * it straight from `useGradientPicker` and never sees a `stopEditor` prop.\n * A sibling context lets a barrel inject the default once, above either\n * provider, without threading the renderer through the headless state.\n */\nexport const GradientStopEditorContext =\n  React.createContext<GradientStopEditorRenderer | null>(null);\n\n/** The injected stop editor, or `null` when no barrel/consumer provided one. */\nexport function useGradientStopEditor(): GradientStopEditorRenderer | null {\n  return React.useContext(GradientStopEditorContext);\n}\n\nexport function useGradientPickerContext(): GradientPickerState {\n  const ctx = React.useContext(GradientPickerContext);\n  if (!ctx) {\n    throw new Error(\n      \"GradientPicker.* parts must be rendered inside <GradientPicker.Root> \" +\n        \"or <FillPicker.Pane mode=\\\"gradient\\\">\",\n    );\n  }\n  return ctx;\n}\n"
    },
    {
      "path": "registry/new-york/ui/fill-picker/hooks/use-gradient-picker.ts",
      "type": "registry:hook",
      "target": "@ui/fill-picker/hooks/use-gradient-picker.ts",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n  DEFAULT_LINEAR,\n  DEFAULT_RADIAL,\n  DEFAULT_CONIC,\n  angleFromPoints,\n  formatGradient,\n  type Gradient,\n  type GradientInterp,\n  type GradientStop,\n  type GradientType,\n  type LinearGradient,\n  type RadialGradient,\n  type RadialSizeKeyword,\n  type ConicGradient,\n} from \"../lib/gradient\";\nimport type { ColorFormat, OklchColor } from \"../lib/types\";\n\nlet __idCounter = 0;\nconst nextId = () => `s${++__idCounter}`;\n\n// ---- Internal types --------------------------------------------------------\n\ninterface InternalStop extends GradientStop {\n  id: string;\n}\n\n// We store gradient config and stops separately so TypeScript always sees\n// stops as InternalStop[] without needing to narrow through a discriminated union.\ninterface InternalState {\n  // The gradient without stops — used as a base for type-specific properties.\n  // We keep the full gradient object and overlay stops separately.\n  gradient: Gradient;\n  stops: InternalStop[];\n}\n\n// ---- Helpers ---------------------------------------------------------------\n\nfunction attachIds(g: Gradient): InternalState {\n  // Sort on entry. `formatStops` emits stops in array order and CSS requires\n  // non-decreasing positions, so an out-of-order (but perfectly type-legal)\n  // stop array arriving via `defaultValue`, a controlled `value`, or\n  // `setGradient` would emit CSS the browser silently clamps into a flat\n  // ramp. The mutating setters (addStop / moveStop / reverseStops) already\n  // route through `sortByPosition` for exactly this reason — the entry points\n  // did not. `gradient.stops` is never read for stop data (toPublicGradient\n  // always rebuilds it from `stops`), so sorting the id-bearing array is\n  // sufficient.\n  const used = new Set<string>();\n  return {\n    gradient: g,\n    stops: [...g.stops]\n      .sort((a, b) => a.position - b.position)\n      .map((s) => {\n        // Honor a consumer-supplied id so controlled reconciliation can match\n        // on identity. Duplicates would defeat the point (and could collide\n        // with a generated id), so anything already taken falls back.\n        const id = s.id && !used.has(s.id) ? s.id : nextId();\n        used.add(id);\n        return { ...s, id };\n      }),\n  };\n}\n\n/**\n * True when the caller is tracking stops by id — i.e. every stop of the\n * gradient they handed us carries one. Partial id sets don't count: the\n * identity match is all-or-nothing, so half-tagged stops would silently fall\n * back to position matching anyway.\n */\nfunction hasStopIds(g: Gradient): boolean {\n  return g.stops.length > 0 && g.stops.every((s) => !!s.id);\n}\n\n/**\n * `withIds` echoes ids back out. Off by default so a caller who never opted\n * into `GradientStop.id` keeps receiving exactly the object shape they always\n * have. Once they *have* opted in, stripping ids would defeat the feature for\n * the common `onValueChange={g => setG(g)}` pattern: the stored gradient would\n * come back untagged on the next render and fall straight back to position\n * matching. Stops added inside the picker are echoed too — a half-tagged set\n * is the same as none.\n */\nfunction toPublicGradient(s: InternalState, withIds: boolean): Gradient {\n  const stops: GradientStop[] = withIds\n    ? s.stops.map((stop) => ({ ...stop }))\n    : s.stops.map(({ id: _id, ...rest }) => rest);\n  return { ...s.gradient, stops } as Gradient;\n}\n\nfunction sortByPosition(stops: InternalStop[]): InternalStop[] {\n  return [...stops].sort((a, b) => a.position - b.position);\n}\n\nconst clamp01 = (n: number) => Math.max(0, Math.min(1, n));\n\nfunction defaultsForType(type: GradientType): Gradient {\n  if (type === \"linear\") return DEFAULT_LINEAR;\n  if (type === \"radial\") return DEFAULT_RADIAL;\n  return DEFAULT_CONIC;\n}\n\n// ---- Public API types -------------------------------------------------------\n\nexport interface UseGradientPickerProps {\n  value?: Gradient;\n  defaultValue?: Gradient;\n  onValueChange?: (gradient: Gradient, css: string) => void;\n  /**\n   * Initial display format applied to every stop on mount. Each stop\n   * tracks its **own** format from then on — changing format in one\n   * stop's popover (or in `<GradientPicker.StopColor>`'s FormatSwitcher,\n   * which is bound to the selected stop) only updates that stop's row\n   * text. Defaults to `oklch` (lossless across the full P3/Rec.2020\n   * gamut stop colors can occupy).\n   */\n  defaultStopColorFormat?: ColorFormat;\n}\n\nexport interface GradientPickerState {\n  gradient: Gradient;\n  stops: InternalStop[];\n  selectedStopId: string;\n  selectedStop: InternalStop | null;\n  setGradient: (next: Gradient) => void;\n  setType: (type: GradientType) => void;\n  setAngle: (angleDeg: number) => void;\n  /**\n   * Set the start endpoint of a linear gradient (normalized 0..1 of the\n   * gradient box). Promotes the gradient to \"positioned\" mode — when both\n   * `start` and `end` are set, the gradient is treated as a line between\n   * them rather than an angle-only construct. `angle` is kept in sync with\n   * the derived direction. Passing `undefined` clears the override and\n   * returns the gradient to the legacy angle-only behavior.\n   */\n  setLinearStart: (xy: { x: number; y: number } | undefined) => void;\n  /** Set the end endpoint of a linear gradient. See `setLinearStart`. */\n  setLinearEnd: (xy: { x: number; y: number } | undefined) => void;\n  setStartAngle: (angleDeg: number) => void;\n  setCenter: (xy: { x: number; y: number }) => void;\n  setInterp: (interp: GradientInterp) => void;\n  /**\n   * Toggle the `repeating` flag on the active gradient. When true,\n   * `formatGradient` emits `repeating-<type>-gradient(...)`. Stops are\n   * unaffected — the stop ramp simply tiles instead of stretching to fill\n   * the box.\n   */\n  setRepeating: (repeating: boolean) => void;\n  setRadialShape: (shape: \"circle\" | \"ellipse\") => void;\n  setRadialSize: (size: RadialSizeKeyword) => void;\n  /**\n   * Set explicit numeric radii on the active radial gradient. Each value is\n   * a 0..1 fraction of the gradient box (x→width, y→height). Passing\n   * `undefined` clears the explicit override and falls back to the\n   * keyword-based size from `setRadialShape` / `setRadialSize`. Use this\n   * for ellipse-shape radials.\n   */\n  setRadii: (radii: { x: number; y: number } | undefined) => void;\n  /**\n   * Set the explicit circle radius in absolute pixels. Only meaningful\n   * when `shape === \"circle\"` — produces a CSS `radial-gradient(<px>px\n   * at ...)` form, which is the only way to keep the gradient visually\n   * circular in any consumer container (the percentage pair form always\n   * implies ellipse). Passing `undefined` clears the override.\n   */\n  setRadiusPx: (px: number | undefined) => void;\n  /**\n   * Latest reported width of the visual gradient box (px). Set by\n   * `<GradientPicker.Area>` via its `ResizeObserver`. Used by parts that\n   * want to display radius as a percentage of the picker box (e.g. the\n   * radius input on `<GradientPicker.RadiusInput>`) — they read this to\n   * convert between the absolute-px storage and a friendlier % display.\n   * `null` when no Area is currently mounted.\n   */\n  containerWidth: number | null;\n  setContainerWidth: (width: number | null) => void;\n  selectStop: (id: string) => void;\n  addStop: (position: number, color?: OklchColor) => string;\n  removeStop: (id: string) => void;\n  moveStop: (id: string, position: number) => void;\n  setStopColor: (id: string, color: OklchColor) => void;\n  setStopHint: (id: string, hint: number | undefined) => void;\n  reverseStops: () => void;\n  /**\n   * Per-stop display format. Each row tracks its own — changing format\n   * in one stop's popover (or in `<GradientPicker.StopColor>`'s\n   * FormatSwitcher, which is bound to the selected stop) only updates\n   * that stop. Stops without an explicit entry fall back to\n   * `defaultStopColorFormat` (default `oklch`).\n   */\n  getStopColorFormat: (id: string) => ColorFormat;\n  setStopColorFormat: (id: string, format: ColorFormat) => void;\n}\n\n// ---- Hook ------------------------------------------------------------------\n\nexport function useGradientPicker(\n  props: UseGradientPickerProps = {},\n): GradientPickerState {\n  const { value, defaultValue, onValueChange, defaultStopColorFormat } = props;\n  const isControlled = value !== undefined;\n  const fallbackFormat: ColorFormat = defaultStopColorFormat ?? \"oklch\";\n  const [stopColorFormats, setStopColorFormats] = React.useState<\n    Record<string, ColorFormat>\n  >({});\n  const getStopColorFormat = React.useCallback(\n    (id: string): ColorFormat => stopColorFormats[id] ?? fallbackFormat,\n    [stopColorFormats, fallbackFormat],\n  );\n  const setStopColorFormat = React.useCallback(\n    (id: string, format: ColorFormat) =>\n      setStopColorFormats((prev) =>\n        prev[id] === format ? prev : { ...prev, [id]: format },\n      ),\n    [],\n  );\n\n  const [internal, setInternal] = React.useState<InternalState>(() =>\n    attachIds(value ?? defaultValue ?? DEFAULT_LINEAR),\n  );\n\n  // Whether the caller opted into id tracking. State drives render output;\n  // the mirrored ref lets `apply` handle chained mutations synchronously.\n  const initialIdTracking = hasStopIds(\n    value ?? defaultValue ?? DEFAULT_LINEAR,\n  );\n  const [idTracked, setIdTracked] = React.useState(initialIdTracking);\n  const idTrackedRef = React.useRef(initialIdTracking);\n  const [selectedStopId, setSelectedStopId] = React.useState<string>(\n    () => internal.stops[0]?.id ?? \"\",\n  );\n\n  // Track the last gradient we emitted upward so the controlled-sync path can\n  // ignore echoes. Seed with the initial controlled value so the *first* sync\n  // is treated as an echo of our own initial state.\n  const [lastEmitted, setLastEmitted] = React.useState<Gradient | null>(\n    value ?? null,\n  );\n\n  // stateRef mirrors the last committed `internal` value so setters can\n  // compute and emit synchronously. `apply` also advances it immediately so\n  // chained setters in one event handler each see the previous result.\n  const stateRef = React.useRef(internal);\n\n  // Latest-callback ref so a fresh arrow each render doesn't re-create setters.\n  const onValueChangeRef = React.useRef(onValueChange);\n\n  // Per-shape override stashes. The radial gradient can carry either an\n  // ellipse override (`radii`) or a circle override (`radiusPx`), never\n  // both at once — `setRadii` / `setRadiusPx` cross-clear. But emit logic\n  // in `formatGradient` checks the override fields before the `shape`\n  // flag, so a stale override left over from the previous shape would\n  // silently keep drawing the old ending shape. We stash whichever\n  // override belongs to the shape we're leaving, strip both off the\n  // gradient, then restore the target shape's stash. Toggling back gives\n  // the user their last numeric value instead of nothing.\n  const radiiStashRef = React.useRef<{ x: number; y: number } | undefined>(\n    undefined,\n  );\n  const radiusPxStashRef = React.useRef<number | undefined>(undefined);\n  const [stashResetVersion, setStashResetVersion] = React.useState(0);\n\n  React.useLayoutEffect(() => {\n    stateRef.current = internal;\n    idTrackedRef.current = idTracked;\n    onValueChangeRef.current = onValueChange;\n  }, [idTracked, internal, onValueChange]);\n\n  React.useLayoutEffect(() => {\n    if (stashResetVersion === 0) return;\n    radiiStashRef.current = undefined;\n    radiusPxStashRef.current = undefined;\n  }, [stashResetVersion]);\n\n  // Sync controlled value during render (the \"adjusting state during render\"\n  // pattern from React docs) instead of an effect. An effect would commit a\n  // render with stale `internal`, then schedule a second render — children\n  // would see one frame with the previous gradient. Doing the reconcile here\n  // means the same render that observes a new `value` also renders with the\n  // reconciled internal state.\n  const [prevControlledValue, setPrevControlledValue] = React.useState<\n    Gradient | undefined\n  >(value);\n  if (isControlled && value !== prevControlledValue) {\n    setPrevControlledValue(value);\n    // An echo is consumed exactly once: a parent that later hands back the\n    // same retained object (undo/redo) is describing a real change.\n    if (lastEmitted !== null) setLastEmitted(null);\n    if (value !== lastEmitted) {\n      const prev = internal;\n      // `prev.stops` is always position-sorted (attachIds and every mutating\n      // setter sort), so the incoming array must be sorted the same way\n      // before the element-wise compare. Without this, a controlled consumer\n      // who keeps their stops in insertion order rather than position order\n      // fails the match on *every* update and gets fresh ids each time —\n      // which orphans `selectedStopId` and any per-stop color format.\n      //\n      // Without ids, stops are re-paired by index, so two sharing a position\n      // are disambiguated only by array order: swap two coincident stops and\n      // the ids stay put while the colors move between them. That is\n      // long-standing behavior (unchanged by the sort) and is pinned by the\n      // `stop identity with duplicate positions` tests. Consumers who need\n      // exactness opt into `GradientStop.id` and take the identity path\n      // below.\n      const incoming = [...value.stops].sort(\n        (a, b) => a.position - b.position,\n      );\n      // Identity path: when every incoming stop carries an id and that set is\n      // exactly what we hold, pair on the id. This is the only way to follow\n      // stops through a reorder that position+index cannot describe — two\n      // stops sharing a position, most obviously.\n      const prevIds = new Set(prev.stops.map((s) => s.id));\n      const sameLength = prev.stops.length === incoming.length;\n      // Ids, when supplied, are authoritative: a changed id set means the\n      // consumer is describing different stops, even if the positions happen\n      // to line up. Falling back to the position path there would ignore the\n      // identity the consumer just handed us.\n      const allHaveIds = incoming.length > 0 && incoming.every((s) => !!s.id);\n      // Duplicated ids can't identify anything — treat them as un-tagged\n      // rather than aliasing two stops onto one id.\n      const uniqueIds =\n        allHaveIds && new Set(incoming.map((s) => s.id)).size === incoming.length;\n      const byId =\n        uniqueIds && sameLength && incoming.every((s) => prevIds.has(s.id!));\n      const sameShape = uniqueIds\n        ? byId\n        : sameLength &&\n          prev.stops.every((s, i) => s.position === incoming[i].position);\n      setIdTracked(uniqueIds);\n      const structuralMatch = prev.gradient.type === value.type && sameShape;\n      const next: InternalState = structuralMatch\n        ? {\n            gradient: value,\n            stops: byId\n              ? incoming.map((s) => ({ ...s, id: s.id as string }))\n              : prev.stops.map((s, i) => ({ ...incoming[i], id: s.id })),\n          }\n        : attachIds(value);\n      if (!structuralMatch) {\n        setStashResetVersion((version) => version + 1);\n        // Fresh ids orphan the selection; keep the same slot selected.\n        if (!next.stops.some((s) => s.id === selectedStopId)) {\n          const index = prev.stops.findIndex((s) => s.id === selectedStopId);\n          const fallback =\n            next.stops[Math.min(Math.max(index, 0), next.stops.length - 1)];\n          if (fallback) setSelectedStopId(fallback.id);\n        }\n      }\n      setInternal(next);\n    }\n  }\n\n  // Apply a state transition + emit in one shot. Read-modify-write through\n  // stateRef so chained calls in one event handler each see the previous\n  // result. Returning `prev` (or null) is a no-op signal.\n  const apply = React.useCallback(\n    (compute: (prev: InternalState) => InternalState | null) => {\n      const prev = stateRef.current;\n      const next = compute(prev);\n      if (next === null || next === prev) return;\n      stateRef.current = next;\n      setInternal(next);\n      const clean = toPublicGradient(next, idTrackedRef.current);\n      setLastEmitted(clean);\n      onValueChangeRef.current?.(clean, formatGradient(clean));\n    },\n    [],\n  );\n\n  // ---- Gradient-level setters ----------------------------------------------\n\n  const setGradient = React.useCallback(\n    (next: Gradient) => {\n      // Wholesale gradient replacement invalidates per-shape stashes — the\n      // user is handing us a brand-new gradient, not toggling the current one.\n      radiiStashRef.current = undefined;\n      radiusPxStashRef.current = undefined;\n      const tracksIds = hasStopIds(next);\n      idTrackedRef.current = tracksIds;\n      setIdTracked(tracksIds);\n      apply(() => attachIds(next));\n      setSelectedStopId((prev) => stateRef.current.stops[0]?.id ?? prev);\n    },\n    [apply],\n  );\n\n  const setType = React.useCallback(\n    (type: GradientType) =>\n      apply((prev) => {\n        if (prev.gradient.type === type) return prev;\n        radiiStashRef.current = undefined;\n        radiusPxStashRef.current = undefined;\n        const fallback = defaultsForType(type);\n        return {\n          gradient: { ...fallback, interp: prev.gradient.interp } as Gradient,\n          stops: prev.stops,\n        };\n      }),\n    [apply],\n  );\n\n  const setAngle = React.useCallback(\n    (angleDeg: number) =>\n      apply((prev) => {\n        if (prev.gradient.type !== \"linear\") return prev;\n        const cur = prev.gradient as LinearGradient;\n        // Setting an angle directly clears any free-position endpoints — the\n        // angle-only model can't represent both. Callers that want to\n        // preserve positioned mode should `setLinearStart` / `setLinearEnd`\n        // (which keep `angle` in sync with the derived direction).\n        const { start: _s, end: _e, ...rest } = cur;\n        const next: LinearGradient = { ...rest, angle: angleDeg };\n        return { gradient: next, stops: prev.stops };\n      }),\n    [apply],\n  );\n\n  const setLinearStart = React.useCallback(\n    (xy: { x: number; y: number } | undefined) =>\n      apply((prev) => {\n        if (prev.gradient.type !== \"linear\") return prev;\n        const cur = prev.gradient as LinearGradient;\n        const next: LinearGradient = xy\n          ? {\n              ...cur,\n              start: { x: clamp01(xy.x), y: clamp01(xy.y) },\n              angle: angleFromPoints(\n                { x: clamp01(xy.x), y: clamp01(xy.y) },\n                cur.end,\n              ) ?? cur.angle,\n            }\n          : (() => {\n              const { start: _drop, ...rest } = cur;\n              return rest as LinearGradient;\n            })();\n        return { gradient: next, stops: prev.stops };\n      }),\n    [apply],\n  );\n\n  const setLinearEnd = React.useCallback(\n    (xy: { x: number; y: number } | undefined) =>\n      apply((prev) => {\n        if (prev.gradient.type !== \"linear\") return prev;\n        const cur = prev.gradient as LinearGradient;\n        const next: LinearGradient = xy\n          ? {\n              ...cur,\n              end: { x: clamp01(xy.x), y: clamp01(xy.y) },\n              angle: angleFromPoints(\n                cur.start,\n                { x: clamp01(xy.x), y: clamp01(xy.y) },\n              ) ?? cur.angle,\n            }\n          : (() => {\n              const { end: _drop, ...rest } = cur;\n              return rest as LinearGradient;\n            })();\n        return { gradient: next, stops: prev.stops };\n      }),\n    [apply],\n  );\n\n  const setStartAngle = React.useCallback(\n    (angleDeg: number) =>\n      apply((prev) => {\n        if (prev.gradient.type !== \"conic\") return prev;\n        return {\n          gradient: { ...(prev.gradient as ConicGradient), startAngle: angleDeg },\n          stops: prev.stops,\n        };\n      }),\n    [apply],\n  );\n\n  const setCenter = React.useCallback(\n    (xy: { x: number; y: number }) =>\n      apply((prev) => {\n        if (prev.gradient.type === \"linear\") return prev;\n        return {\n          gradient: {\n            ...(prev.gradient as RadialGradient | ConicGradient),\n            center: { x: clamp01(xy.x), y: clamp01(xy.y) },\n          },\n          stops: prev.stops,\n        };\n      }),\n    [apply],\n  );\n\n  const setInterp = React.useCallback(\n    (interp: GradientInterp) =>\n      apply((prev) => ({\n        gradient: { ...prev.gradient, interp } as Gradient,\n        stops: prev.stops,\n      })),\n    [apply],\n  );\n\n  const setRepeating = React.useCallback(\n    (repeating: boolean) =>\n      apply((prev) => {\n        const cur = prev.gradient;\n        // Strip the flag entirely when toggled off so the gradient stays\n        // structurally identical to one that never had it — keeps equality\n        // checks and serialization minimal.\n        if (!repeating) {\n          if (!cur.repeating) return prev;\n          const { repeating: _drop, ...rest } = cur;\n          return { gradient: rest as Gradient, stops: prev.stops };\n        }\n        if (cur.repeating) return prev;\n        return {\n          gradient: { ...cur, repeating: true } as Gradient,\n          stops: prev.stops,\n        };\n      }),\n    [apply],\n  );\n\n  const setRadialShape = React.useCallback(\n    (shape: \"circle\" | \"ellipse\") =>\n      apply((prev) => {\n        if (prev.gradient.type !== \"radial\") return prev;\n        const cur = prev.gradient as RadialGradient;\n        if (cur.shape === shape) return prev;\n        if (cur.shape === \"circle\") {\n          radiusPxStashRef.current = cur.radiusPx;\n        } else {\n          radiiStashRef.current = cur.radii;\n        }\n        const { radii: _r, radiusPx: _px, ...rest } = cur;\n        const next: RadialGradient =\n          shape === \"circle\"\n            ? {\n                ...rest,\n                shape,\n                ...(radiusPxStashRef.current !== undefined\n                  ? { radiusPx: radiusPxStashRef.current }\n                  : {}),\n              }\n            : {\n                ...rest,\n                shape,\n                ...(radiiStashRef.current\n                  ? { radii: radiiStashRef.current }\n                  : {}),\n              };\n        return { gradient: next, stops: prev.stops };\n      }),\n    [apply],\n  );\n\n  const setRadialSize = React.useCallback(\n    (size: RadialSizeKeyword) =>\n      apply((prev) => {\n        if (prev.gradient.type !== \"radial\") return prev;\n        const base = prev.gradient as RadialGradient;\n        // The keyword form and the explicit `radii` / `radiusPx` overrides\n        // are mutually exclusive at emit time — `formatGradient` reads the\n        // overrides first and never falls through to `shape size`. Picking\n        // a size keyword is the user explicitly opting back into the\n        // keyword path, so drop the numeric overrides (and the stash) in\n        // the same commit. Without this, the dropdown changes the model\n        // field but the rendered CSS doesn't move.\n        radiiStashRef.current = undefined;\n        radiusPxStashRef.current = undefined;\n        const { radii: _r, radiusPx: _px, ...rest } = base;\n        return {\n          gradient: { ...rest, size } as RadialGradient,\n          stops: prev.stops,\n        };\n      }),\n    [apply],\n  );\n\n  const setRadii = React.useCallback(\n    (radii: { x: number; y: number } | undefined) =>\n      apply((prev) => {\n        if (prev.gradient.type !== \"radial\") return prev;\n        const base = prev.gradient as RadialGradient;\n        // Setting `radii` is the ellipse path. Three jobs:\n        //   1. Clear `radiusPx` so the two override fields never coexist.\n        //   2. Enforce `shape: \"ellipse\"` — `formatGradient` emits\n        //      `ellipse <rx> <ry>` regardless of `shape` when `radii` is\n        //      present, so anything else would desync the model from CSS\n        //      (and from the ShapeSwitcher / radius input UI).\n        //   3. If the previous shape was circle, stash its `radiusPx` so the\n        //      user can flip back via ShapeSwitcher without losing it (the\n        //      stash matches `setRadialShape`'s own preservation behavior).\n        if (base.shape === \"circle\" && base.radiusPx !== undefined) {\n          radiusPxStashRef.current = base.radiusPx;\n        }\n        const { radii: _dropRadii, radiusPx: _dropPx, ...rest } = base;\n        const next: RadialGradient = radii\n          ? {\n              ...rest,\n              shape: \"ellipse\",\n              radii: { x: Math.max(0, radii.x), y: Math.max(0, radii.y) },\n            }\n          : ({ ...rest, shape: \"ellipse\" } as RadialGradient);\n        return { gradient: next, stops: prev.stops };\n      }),\n    [apply],\n  );\n\n  const [containerWidth, setContainerWidth] =\n    React.useState<number | null>(null);\n\n  const setRadiusPx = React.useCallback(\n    (px: number | undefined) =>\n      apply((prev) => {\n        if (prev.gradient.type !== \"radial\") return prev;\n        const base = prev.gradient as RadialGradient;\n        // Setting `radiusPx` is the circle path. Mirror `setRadii`:\n        // clear `radii`, enforce `shape: \"circle\"` (so the model can't\n        // disagree with CSS / the radius input), and stash any previous\n        // ellipse radii so ShapeSwitcher → ellipse restores them.\n        if (base.shape === \"ellipse\" && base.radii) {\n          radiiStashRef.current = base.radii;\n        }\n        const { radii: _dropRadii, radiusPx: _dropPx, ...rest } = base;\n        const next: RadialGradient =\n          px !== undefined\n            ? { ...rest, shape: \"circle\", radiusPx: Math.max(0, px) }\n            : ({ ...rest, shape: \"circle\" } as RadialGradient);\n        return { gradient: next, stops: prev.stops };\n      }),\n    [apply],\n  );\n\n  // ---- Stop setters --------------------------------------------------------\n\n  const selectStop = React.useCallback(\n    (id: string) => setSelectedStopId(id),\n    [setSelectedStopId],\n  );\n\n  const addStop = React.useCallback(\n    (position: number, color?: OklchColor): string => {\n      const id = nextId();\n      const fallback: OklchColor = { l: 0.5, c: 0, h: 0, alpha: 1 };\n      apply((prev) => ({\n        gradient: prev.gradient,\n        stops: sortByPosition([\n          ...prev.stops,\n          { id, position, color: color ?? fallback },\n        ]),\n      }));\n      setSelectedStopId(id);\n      return id;\n    },\n    [apply, setSelectedStopId],\n  );\n\n  const removeStop = React.useCallback(\n    (id: string) => {\n      let nextSelId: string | null = null;\n      apply((prev) => {\n        if (prev.stops.length <= 1) return prev;\n        const idx = prev.stops.findIndex((s) => s.id === id);\n        if (idx === -1) return prev;\n        const filtered = prev.stops.filter((s) => s.id !== id);\n        nextSelId = filtered[Math.min(idx, filtered.length - 1)].id;\n        return { gradient: prev.gradient, stops: filtered };\n      });\n      if (nextSelId !== null) setSelectedStopId(nextSelId);\n    },\n    [apply, setSelectedStopId],\n  );\n\n  const moveStop = React.useCallback(\n    (id: string, position: number) => {\n      // Intentionally unclamped: on a positioned linear gradient, authored\n      // positions outside 0..1 extrapolate beyond the [start, end] segment\n      // (legal CSS). UI inputs clamp their own *displayed* coordinates.\n      apply((prev) => {\n        // Move the target stop to the end before sorting so that in a position\n        // tie it sorts after existing stops at that position (stable sort).\n        const others = prev.stops.filter((s) => s.id !== id);\n        const moved = prev.stops.find((s) => s.id === id);\n        if (!moved) return prev;\n        return {\n          gradient: prev.gradient,\n          stops: sortByPosition([...others, { ...moved, position }]),\n        };\n      });\n    },\n    [apply],\n  );\n\n  const setStopColor = React.useCallback(\n    (id: string, color: OklchColor) =>\n      apply((prev) => ({\n        gradient: prev.gradient,\n        stops: prev.stops.map((s) => (s.id === id ? { ...s, color } : s)),\n      })),\n    [apply],\n  );\n\n  const setStopHint = React.useCallback(\n    (id: string, hint: number | undefined) =>\n      apply((prev) => ({\n        gradient: prev.gradient,\n        stops: prev.stops.map((s) => (s.id === id ? { ...s, hint } : s)),\n      })),\n    [apply],\n  );\n\n  const reverseStops = React.useCallback(\n    () =>\n      apply((prev) => {\n        if (prev.stops.length < 2) return prev;\n        // Mirror each stop's position around 0.5 so the visual order flips\n        // while ids stay attached to their colors. A hint lives on the stop\n        // that *follows* its segment, so after the flip the mirrored hint\n        // must move to the segment's other stop (the one that follows in\n        // the new order) — keeping it in place would strand it on a stop\n        // that now leads, where formatStops silently drops it.\n        const movedHints = new Map<string, number>();\n        prev.stops.forEach((s, i) => {\n          if (s.hint !== undefined && i > 0) {\n            movedHints.set(prev.stops[i - 1].id, 1 - s.hint);\n          }\n        });\n        const flipped = prev.stops.map((s) => ({\n          ...s,\n          position: 1 - s.position,\n          hint: movedHints.get(s.id),\n        }));\n        return { gradient: prev.gradient, stops: sortByPosition(flipped) };\n      }),\n    [apply],\n  );\n\n  // ---- Derived values ------------------------------------------------------\n\n  const selectedStop = React.useMemo(\n    () => internal.stops.find((s) => s.id === selectedStopId) ?? null,\n    [internal.stops, selectedStopId],\n  );\n\n  const cleanGradient = React.useMemo(\n    () => toPublicGradient(internal, idTracked),\n    [idTracked, internal],\n  );\n\n  return {\n    gradient: cleanGradient,\n    stops: internal.stops,\n    selectedStopId,\n    selectedStop,\n    setGradient,\n    setType,\n    setAngle,\n    setLinearStart,\n    setLinearEnd,\n    setStartAngle,\n    setCenter,\n    setInterp,\n    setRepeating,\n    setRadialShape,\n    setRadialSize,\n    setRadii,\n    setRadiusPx,\n    containerWidth,\n    setContainerWidth,\n    selectStop,\n    addStop,\n    removeStop,\n    moveStop,\n    setStopColor,\n    setStopHint,\n    reverseStops,\n    getStopColorFormat,\n    setStopColorFormat,\n  };\n}\n"
    },
    {
      "path": "registry/new-york/ui/fill-picker/parts/gradient/root.tsx",
      "type": "registry:ui",
      "target": "@ui/fill-picker/parts/gradient/root.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  GradientPickerContext,\n  GradientStopEditorContext,\n  useGradientStopEditor,\n  type GradientStopEditorRenderer,\n} from \"../../contexts/gradient\";\nimport {\n  useGradientPicker,\n  type UseGradientPickerProps,\n} from \"../../hooks/use-gradient-picker\";\n\nexport interface RootProps\n  extends UseGradientPickerProps,\n    Omit<React.HTMLAttributes<HTMLDivElement>, \"defaultValue\" | \"onChange\"> {\n  /**\n   * Editor mounted around each `<Bar editOnClick>` stop handle. Each barrel\n   * (`GradientPicker` / `GradientPickerBase`) injects its own variant's editor\n   * as the default; pass this to substitute your own. Omitted, the editor is\n   * inherited from an enclosing provider — and with none (a deep-imported\n   * engine `Root`), the Bar renders bare handles.\n   */\n  stopEditor?: GradientStopEditorRenderer;\n}\n\nexport const Root = React.forwardRef<HTMLDivElement, RootProps>(function Root(\n  {\n    value,\n    defaultValue,\n    onValueChange,\n    defaultStopColorFormat,\n    stopEditor,\n    className,\n    children,\n    ...rest\n  },\n  ref,\n) {\n  const state = useGradientPicker({\n    value,\n    defaultValue,\n    onValueChange,\n    defaultStopColorFormat,\n  });\n  // Omitting the prop inherits whatever editor an outer provider (a barrel's\n  // `FillPicker.Pane`, say) already injected, rather than blanking it — a\n  // deep-imported engine Root nested in a wired-up tree keeps its editor.\n  const inherited = useGradientStopEditor();\n  const editor = stopEditor ?? inherited;\n  return (\n    <GradientStopEditorContext.Provider value={editor}>\n      <GradientPickerContext.Provider value={state}>\n        <div\n          ref={ref}\n          data-slot=\"gradient-picker\"\n          className={cn(\n            \"flex w-full max-w-[280px] flex-col gap-3 rounded-lg border border-border bg-popover p-3 text-popover-foreground shadow-sm\",\n            className,\n          )}\n          {...rest}\n        >\n          {children}\n        </div>\n      </GradientPickerContext.Provider>\n    </GradientStopEditorContext.Provider>\n  );\n});\n"
    },
    {
      "path": "registry/new-york/ui/fill-picker/parts/gradient/bar.tsx",
      "type": "registry:ui",
      "target": "@ui/fill-picker/parts/gradient/bar.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  useGradientPickerContext,\n  useGradientStopEditor,\n} from \"../../contexts/gradient\";\nimport {\n  adjustStopsForEndpoints,\n  formatGradient,\n  projectStopPosition,\n  reverseProjectStopPosition,\n  sampleStopsAt,\n  type LinearGradient,\n} from \"../../lib/gradient\";\nimport { formatColor } from \"../../lib/color\";\nimport { focusNeighborIn } from \"./stop-list-shared\";\nimport { useLiveAnnounce } from \"../use-live-announce\";\n\nexport interface BarProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** Track height in px. Defaults to 12 to match `<ColorPicker.Hue>`. */\n  height?: number;\n  /** Handle (stop) diameter in px. Defaults to 16 to match the Hue thumb. */\n  handleSize?: number;\n  /**\n   * Open the stop-color editor popover when a handle is clicked without\n   * being dragged. Drag is still the canonical way to reposition a stop —\n   * the click-vs-drag distinction is movement-based, so flicking the\n   * handle even a pixel keeps the popover closed. Defaults to `false`.\n   */\n  editOnClick?: boolean;\n}\n\nfunction buildPreviewGradient(\n  g: ReturnType<typeof useGradientPickerContext>[\"gradient\"],\n): string {\n  // For the bar preview we always render the stops as a horizontal linear\n  // gradient regardless of the live gradient type — the bar is the editing\n  // surface for stop positions. When the live gradient is a positioned\n  // linear with `start`/`end`, mirror the same stop projection used at CSS\n  // emit time so the Bar shows the *visible* stop positions.\n  const stops =\n    g.type === \"linear\" && g.start && g.end\n      ? adjustStopsForEndpoints(g.stops, g.start, g.end)\n      : g.stops;\n  const linear: LinearGradient = {\n    type: \"linear\",\n    angle: 90,\n    interp: g.interp,\n    stops,\n  };\n  return formatGradient(linear);\n}\n\nexport const Bar = React.forwardRef<HTMLDivElement, BarProps>(function Bar(\n  { className, height = 12, handleSize = 16, editOnClick = false, ...rest },\n  ref,\n) {\n  // Which stop's editor popover is open (null = none). Only meaningful\n  // when `editOnClick` is true.\n  const [openStopId, setOpenStopId] = React.useState<string | null>(null);\n  const ctx = useGradientPickerContext();\n  const stopEditor = useGradientStopEditor();\n  const wrapperRef = React.useRef<HTMLDivElement | null>(null);\n  const [liveText, announce] = useLiveAnnounce();\n  const trackRef = React.useRef<HTMLDivElement | null>(null);\n  // Cleanup hook for an active drag's document-level listeners. Lets us tear\n  // them down on unmount so a mid-drag remove of the Bar doesn't leave them\n  // attached to `document` and firing `ctx.moveStop` on a stale context.\n  const activeDragCleanupRef = React.useRef<(() => void) | null>(null);\n  React.useEffect(\n    () => () => {\n      activeDragCleanupRef.current?.();\n      activeDragCleanupRef.current = null;\n    },\n    [],\n  );\n  // Forward the consumer's ref to the outer wrapper (the element that carries\n  // `data-slot=\"gradient-bar\"`) so `wrapper.querySelector('[data-slot=\"gradient-bar\"]')`\n  // works as expected; pointer math still uses the inner trackRef.\n  React.useImperativeHandle(ref, () => wrapperRef.current as HTMLDivElement);\n\n  // When the live gradient is positioned-linear, stop positions are projected\n  // onto the CSS gradient line for display + pointer math. Authored values\n  // (what's written to `stop.position`) stay 0..1 along the segment.\n  const linear = ctx.gradient.type === \"linear\" ? ctx.gradient : null;\n  const start = linear?.start;\n  const end = linear?.end;\n  const toDisplay = (authored: number) =>\n    projectStopPosition(authored, start, end);\n  const fromDisplay = (displayed: number) =>\n    reverseProjectStopPosition(displayed, start, end);\n\n  const displayedPositionFromEvent = React.useCallback(\n    (clientX: number): number => {\n      const el = trackRef.current;\n      if (!el) return 0;\n      const rect = el.getBoundingClientRect();\n      if (rect.width <= 0) return 0;\n      return Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));\n    },\n    [],\n  );\n\n  // A stop added by clicking the track becomes the selected stop, but the\n  // click landed on the track — which isn't focusable — so focus stays on\n  // <body> and the new stop can't be nudged or deleted from the keyboard.\n  // Its handle doesn't exist until the next render, hence the deferral.\n  const pendingFocusIdRef = React.useRef<string | null>(null);\n  React.useLayoutEffect(() => {\n    const id = pendingFocusIdRef.current;\n    if (!id) return;\n    pendingFocusIdRef.current = null;\n    // Matched by walking the handles rather than an attribute selector:\n    // stop ids can come from the consumer now, so they aren't guaranteed\n    // to be selector-safe.\n    const handles = wrapperRef.current?.querySelectorAll<HTMLElement>(\n      '[role=\"slider\"]',\n    );\n    handles?.forEach((h) => {\n      if (h.dataset.stopId === id) h.focus();\n    });\n  });\n\n  const onTrackPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {\n    if (e.target !== trackRef.current) return; // handles handle their own drag\n    // Canceling pointerdown suppresses the compatibility mousedown, and with\n    // it the browser's default focus handling. Without this the layout effect\n    // below focuses the new handle and the browser immediately takes it back\n    // — clicking a non-focusable element blurs to <body> — so the stop you\n    // just placed still wouldn't take Delete or the arrow keys.\n    e.preventDefault();\n    const displayed = displayedPositionFromEvent(e.clientX);\n    // No clamp on the authored value: on a positioned linear, clicks outside\n    // the projected [start, end] segment extrapolate (< 0 / > 1 are legal\n    // CSS stop positions), so the stop lands where the user clicked instead\n    // of stacking on the first/last stop. Plain mode is unaffected —\n    // fromDisplay is the identity and `displayed` is already 0..1.\n    const authored = fromDisplay(displayed);\n    pendingFocusIdRef.current = ctx.addStop(\n      authored,\n      sampleStopsAt(ctx.stops, authored),\n    );\n  };\n\n  const startStopDrag = (id: string) => (e: React.PointerEvent<HTMLDivElement>) => {\n    e.stopPropagation();\n    // preventDefault keeps the browser from starting a text selection or a\n    // native drag mid-gesture, but it also suppresses the focus a pointerdown\n    // would normally give the handle. Focus it explicitly: without this a\n    // clicked stop looks selected yet keyboard keys (nudge, Delete) go to\n    // <body> and do nothing.\n    e.preventDefault();\n    e.currentTarget.focus();\n    ctx.selectStop(id);\n\n    // Listen on document, not the handle DOM node. moveStop reorders the\n    // stops array, and React reconciles the per-stop <div>s in their new\n    // order; if we captured/listened on the original handle element, a\n    // sibling handle could end up under the pointer and steal the gesture.\n    // Document-level listeners + an `id` captured in closure keep the drag\n    // bound to the originally-clicked stop regardless of reordering.\n    let pendingRemove = false;\n    // Track pointer movement so pointerUp can tell a tap from a drag —\n    // 3px slack absorbs the jitter of a deliberate click.\n    const startX = e.clientX;\n    const startY = e.clientY;\n    // Document-level listeners see every pointer; only the one that started\n    // this drag may steer or end it (multi-touch).\n    const dragPointerId = e.pointerId;\n    let moved = false;\n    const onMove = (ev: PointerEvent) => {\n      if (ev.pointerId !== dragPointerId) return;\n      // Self-heal a missed release: without pointer capture, letting go of\n      // the button outside the browser window never delivers pointerup, so\n      // the first re-entry move arrives with no buttons pressed. End the\n      // drag instead of letting the stop chase the cursor.\n      if (ev.buttons === 0) {\n        cleanup();\n        if (pendingRemove) ctx.removeStop(id);\n        return;\n      }\n      if (!moved) {\n        const dx = ev.clientX - startX;\n        const dy = ev.clientY - startY;\n        if (dx * dx + dy * dy > 9) moved = true;\n      }\n      const rect = trackRef.current?.getBoundingClientRect();\n      if (!rect) return;\n      const dy = ev.clientY - rect.bottom;\n      // Mark for removal while dragged below the bar by more than 24px,\n      // but only commit on release so the user can drag back up to cancel.\n      pendingRemove = dy > 24 && ctx.stops.length > 1;\n      if (!pendingRemove && moved) {\n        const displayed = displayedPositionFromEvent(ev.clientX);\n        ctx.moveStop(id, fromDisplay(displayed));\n      }\n    };\n    const cleanup = () => {\n      document.removeEventListener(\"pointermove\", onMove);\n      document.removeEventListener(\"pointerup\", onUp);\n      document.removeEventListener(\"pointercancel\", onUp);\n      activeDragCleanupRef.current = null;\n    };\n    const onUp = (ev: PointerEvent) => {\n      if (ev.pointerId !== dragPointerId) return;\n      cleanup();\n      if (pendingRemove) ctx.removeStop(id);\n      else if (!moved && editOnClick) setOpenStopId(id);\n    };\n    document.addEventListener(\"pointermove\", onMove);\n    document.addEventListener(\"pointerup\", onUp);\n    document.addEventListener(\"pointercancel\", onUp);\n    activeDragCleanupRef.current = cleanup;\n  };\n\n  const onStopKeyDown =\n    (id: string, position: number) => (e: React.KeyboardEvent<HTMLDivElement>) => {\n      const step = e.shiftKey ? 0.05 : 0.01;\n      // Nudge in *displayed* space so a 1% press visibly moves the stop the\n      // same distance on screen regardless of whether the gradient is\n      // positioned-linear or angle-only.\n      const displayed = toDisplay(position);\n      // Clamp in displayed space: keyboard nudges stay on the visible track\n      // (moveStop itself no longer clamps, to allow extrapolated stops).\n      const clamp01 = (n: number) => Math.max(0, Math.min(1, n));\n      if (e.key === \"ArrowLeft\") {\n        e.preventDefault();\n        ctx.moveStop(id, fromDisplay(clamp01(displayed - step)));\n      } else if (e.key === \"ArrowRight\") {\n        e.preventDefault();\n        ctx.moveStop(id, fromDisplay(clamp01(displayed + step)));\n      } else if (e.key === \"Delete\" || e.key === \"Backspace\") {\n        e.preventDefault();\n        // A gradient needs at least two stops; `removeStop` refuses below\n        // that. The StopList's mouse affordance conveys this by rendering\n        // its remove button `disabled` — the Bar handle has no such visual,\n        // so announce instead of failing silently.\n        if (ctx.stops.length <= 1) {\n          announce(\"Cannot remove the last stop\");\n          return;\n        }\n        // Move focus to a neighboring handle *before* removal, mirroring\n        // StopList: the focused node is about to unmount, and without this\n        // focus falls to <body> and keyboard navigation is lost.\n        focusNeighborIn(wrapperRef.current, e.currentTarget, '[role=\"slider\"]');\n        ctx.removeStop(id);\n        announce(\"Stop removed\");\n      }\n    };\n\n  return (\n    <div\n      ref={wrapperRef}\n      data-slot=\"gradient-bar\"\n      className={cn(\"relative w-full select-none\", className)}\n      style={{ height }}\n      {...rest}\n    >\n      <div\n        ref={trackRef}\n        onPointerDown={onTrackPointerDown}\n        className=\"absolute inset-0 touch-none rounded-full border border-border\"\n        style={{\n          background: `${buildPreviewGradient(ctx.gradient)}, repeating-conic-gradient(#bbb 0 25%, #fff 0 50%) 0 0/8px 8px`,\n        }}\n      />\n      {ctx.stops.map((s) => {\n        const selected = s.id === ctx.selectedStopId;\n        const displayed = Math.max(0, Math.min(1, toDisplay(s.position)));\n        const displayedPct = Math.round(displayed * 100);\n        const handle = (\n          <div\n            role=\"slider\"\n            data-stop-id={s.id}\n            aria-label={`Stop at ${displayedPct}%`}\n            aria-valuemin={0}\n            aria-valuemax={100}\n            aria-valuenow={displayedPct}\n            aria-valuetext={`${displayedPct} percent`}\n            aria-orientation=\"horizontal\"\n            aria-current={selected ? \"true\" : undefined}\n            tabIndex={0}\n            onPointerDown={startStopDrag(s.id)}\n            onFocus={() => ctx.selectStop(s.id)}\n            onKeyDown={onStopKeyDown(s.id, s.position)}\n            style={{\n              // Match the Hue thumb's positioning math: inset the centerline\n              // by handleSize/2 so pos=0/1 sit flush with the track edges.\n              left: `calc(${displayed} * (100% - ${handleSize}px) + ${handleSize / 2}px)`,\n              width: handleSize,\n              height: handleSize,\n              background: formatColor(s.color, \"oklch\"),\n            }}\n            className={cn(\n              \"absolute top-1/2 -translate-x-1/2 -translate-y-1/2 cursor-grab touch-none rounded-full border-2 border-white shadow-[0_0_0_1.5px_rgba(0,0,0,0.6)]\",\n              // WCAG 2.5.8: small visual dot, ≥24px pointer target.\n              \"before:absolute before:-inset-1.5 before:content-['']\",\n              selected && \"outline-2 outline-offset-1 outline-ring\",\n            )}\n          />\n        );\n        // The editor is a slot, not an import: the Bar is shared by both\n        // variants, so the dialect-specific editor is injected by the barrel\n        // (see `GradientStopEditorRenderer`). Without one — or without\n        // `editOnClick` — the handle renders bare.\n        if (!editOnClick || !stopEditor)\n          return <React.Fragment key={s.id}>{handle}</React.Fragment>;\n        return (\n          <React.Fragment key={s.id}>\n            {stopEditor({\n              stopId: s.id,\n              open: openStopId === s.id,\n              onOpenChange: (o) => setOpenStopId(o ? s.id : null),\n              children: handle,\n            })}\n          </React.Fragment>\n        );\n      })}\n      <span aria-live=\"polite\" className=\"sr-only\">\n        {liveText}\n      </span>\n    </div>\n  );\n});\n"
    },
    {
      "path": "registry/new-york/ui/fill-picker/parts/gradient/overlay.tsx",
      "type": "registry:ui",
      "target": "@ui/fill-picker/parts/gradient/overlay.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { useGradientPickerContext } from \"../../contexts/gradient\";\nimport { type RadialSizeKeyword } from \"../../lib/gradient\";\nimport { formatColor } from \"../../lib/color\";\nimport { CHECKERBOARD_LG } from \"../../lib/constants\";\nimport { useLiveAnnounce } from \"../use-live-announce\";\nimport { trackPointerDrag } from \"./pointer-drag\";\n\nexport interface OverlayProps extends React.HTMLAttributes<HTMLDivElement> {\n  /**\n   * Pixel radius of the conic \"from-angle\" dial handle measured from the\n   * center handle. Locked on this ring; drag rotates `startAngle`.\n   */\n  conicDialRadius?: number;\n}\n\ninterface XY {\n  x: number;\n  y: number;\n}\n\nconst HANDLE_PX = 14;\n\n// Re-exported so `<GradientPicker.Area>` can paint its own checkerboard\n/**\n * Convert a CSS gradient angle (0deg = up, increases clockwise) to a unit\n * direction vector in screen coordinates (y goes down).\n *   0deg   → ( 0, -1)   up\n *   90deg  → ( 1,  0)   right\n *   180deg → ( 0,  1)   down\n *   270deg → (-1,  0)   left\n */\nfunction angleToDir(angleDeg: number): XY {\n  const r = (angleDeg * Math.PI) / 180;\n  return { x: Math.sin(r), y: -Math.cos(r) };\n}\n\n/**\n * Inverse of `angleToDir`. Returns a CSS gradient angle in [0, 360) given a\n * non-zero direction vector in screen coordinates.\n */\nfunction dirToAngle(dx: number, dy: number): number {\n  // atan2(dx, -dy): 0 = up, increases clockwise — matches AnglePad.\n  const deg = (Math.atan2(dx, -dy) * 180) / Math.PI;\n  return (deg + 360) % 360;\n}\n\n/**\n * Project a ray from the box center along `dir` and return the distance to\n * the box edge. Works for any aspect ratio. Caller must ensure |dir| > 0.\n */\nfunction edgeExtent(dir: XY, halfW: number, halfH: number): number {\n  const ex = dir.x === 0 ? Infinity : halfW / Math.abs(dir.x);\n  const ey = dir.y === 0 ? Infinity : halfH / Math.abs(dir.y);\n  return Math.min(ex, ey);\n}\n\n/** Snap an angle to the nearest `step` degrees, normalized to [0, 360). */\nfunction snapDeg(deg: number, step: number): number {\n  const s = Math.round(deg / step) * step;\n  return ((s % 360) + 360) % 360;\n}\n\n/**\n * Seed numeric radii from the radial gradient's keyword form (`shape` +\n * `size`) so the edge handle has a sensible starting position before the\n * user has touched it. Returns radii as fractions of the box width/height\n * matching the CSS `<length-percentage>{1,2}` convention.\n *\n * For corner-targeting keywords on ellipse we deliberately approximate by\n * stretching each axis to the appropriate side rather than implementing the\n * exact CSS spec — once the user drags, they own the radii anyway.\n */\nexport function keywordToRadii(\n  shape: \"circle\" | \"ellipse\",\n  size: RadialSizeKeyword,\n  center: { x: number; y: number },\n  w: number,\n  h: number,\n): { x: number; y: number } {\n  const cx = center.x * w;\n  const cy = center.y * h;\n  const dxClose = Math.min(cx, w - cx);\n  const dxFar = Math.max(cx, w - cx);\n  const dyClose = Math.min(cy, h - cy);\n  const dyFar = Math.max(cy, h - cy);\n  if (shape === \"circle\") {\n    let rPx: number;\n    switch (size) {\n      case \"closest-side\":\n        rPx = Math.min(dxClose, dyClose);\n        break;\n      case \"closest-corner\":\n        rPx = Math.hypot(dxClose, dyClose);\n        break;\n      case \"farthest-side\":\n        rPx = Math.max(dxFar, dyFar);\n        break;\n      case \"farthest-corner\":\n        rPx = Math.hypot(dxFar, dyFar);\n        break;\n    }\n    return { x: rPx / w, y: rPx / h };\n  }\n  switch (size) {\n    case \"closest-side\":\n      return { x: dxClose / w, y: dyClose / h };\n    case \"closest-corner\":\n      // Ellipse closest-corner: stretch each axis to the *closest* side along\n      // that axis (approximation — the spec scales to actually touch the\n      // closest corner, but visually this is close enough as a seed).\n      return { x: dxClose / w, y: dyClose / h };\n    case \"farthest-side\":\n      return { x: dxFar / w, y: dyFar / h };\n    case \"farthest-corner\":\n      return { x: dxFar / w, y: dyFar / h };\n  }\n}\n\n/**\n * Transparent handle-only overlay that surfaces the *geometry* of the\n * active gradient on top of any element. Designed to be dropped over a\n * consumer's own object (a canvas frame, a div on a design surface,\n * etc.) so the user can edit the gradient in place — same interactions\n * as `<GradientPicker.Area>` but without painting the gradient itself.\n * The host container must establish a positioning context (e.g.\n * `position: relative`) and represent the same coordinate space the\n * gradient will be applied to.\n *\n * Handle behavior by gradient type (identical to the in-Area version):\n *\n * - **Linear** — start / end endpoint handles tinted with the first / last\n *   stop colors. Dragging endpoints rotates the angle (and, on first\n *   drag, promotes to free-position mode). Middle stops are edited on\n *   `<GradientPicker.Bar>`, never on the canvas — the overlay only edits\n *   *geometry*. Consumers mounting an Overlay by itself should pair it\n *   with a `<GradientPicker.Bar>` or `<GradientPicker.StopList>` somewhere\n *   in the same Root, or inner stops won't be editable by pointer at all.\n * - **Radial** — center handle + edge handle. Edge handle drives radius\n *   (px for circles, normalized for ellipses); center moves the\n *   gradient and leaves the keyword `size` alone — keyword extents\n *   (`farthest-corner` etc.) naturally recompute against the new center.\n * - **Conic** — center handle + dial handle for `startAngle`.\n *\n * The root div is `pointer-events-none` so empty regions pass clicks\n * through to whatever sits beneath; the handle buttons themselves\n * intercept events normally. Pair with `<GradientPicker.Area>` for a\n * self-contained preview, or with any custom container for in-place\n * editing on the consumer's canvas.\n */\nexport const Overlay = React.forwardRef<HTMLDivElement, OverlayProps>(\n  function Overlay({ className, conicDialRadius, style, ...rest }, ref) {\n  const ctx = useGradientPickerContext();\n  const { gradient } = ctx;\n  const padRef = React.useRef<HTMLDivElement | null>(null);\n  React.useImperativeHandle(ref, () => padRef.current as HTMLDivElement);\n\n  // Observe the host's dimensions (which the overlay fills via `inset-0`)\n  // so handle geometry stays correct at any aspect ratio.\n  const [dims, setDims] = React.useState<{ w: number; h: number }>({\n    w: 0,\n    h: 0,\n  });\n  const setContainerWidth = ctx.setContainerWidth;\n  React.useEffect(() => {\n    const el = padRef.current;\n    if (!el) return;\n    const ro = new ResizeObserver((entries) => {\n      const entry = entries[0];\n      if (!entry) return;\n      const { width, height: h } = entry.contentRect;\n      setDims({ w: width, h });\n      // Publish width to context so sibling parts (e.g. the radius input\n      // on `<GradientPicker.RadiusInput>`) can convert between absolute\n      // px and a percentage display.\n      setContainerWidth(width);\n    });\n    ro.observe(el);\n    // Intentionally do NOT clear `containerWidth` on unmount: if multiple\n    // Overlays / an <Area> + an Overlay are mounted simultaneously (e.g.\n    // the playground's \"Area + Overlay (demo)\" toggle), the surviving\n    // publisher should keep ownership of the value. The next mount will\n    // overwrite it; until then a stale-but-non-null last-known width is\n    // strictly better than null (which would force percentage inputs to\n    // fall back to px and break the user's display unit mid-session).\n    return () => ro.disconnect();\n  }, [setContainerWidth]);\n\n  // Compute handle positions in *box-local* pixels. Center of the box is\n  // (w/2, h/2). All handle math lives in this coordinate space so we can\n  // reason about it without thinking about page offsets.\n  const handles = React.useMemo(() => {\n    const { w, h } = dims;\n    if (w === 0 || h === 0) return null;\n    const cx = w / 2;\n    const cy = h / 2;\n\n    if (gradient.type === \"linear\") {\n      if (gradient.start && gradient.end) {\n        // Positioned linear: endpoints can sit anywhere inside the box.\n        // Visually clamp by HANDLE_PX/2 so the handle dot stays fully inside\n        // the `overflow-hidden` container even when the user drags to a\n        // corner. The stored `gradient.start` / `gradient.end` keep the\n        // true pointer position — only the rendered handle + dashed line\n        // endpoint are inset.\n        const inset = HANDLE_PX / 2;\n        const clampX = (px: number) => Math.max(inset, Math.min(w - inset, px));\n        const clampY = (px: number) => Math.max(inset, Math.min(h - inset, px));\n        return {\n          a: {\n            x: clampX(gradient.start.x * w),\n            y: clampY(gradient.start.y * h),\n          },\n          b: {\n            x: clampX(gradient.end.x * w),\n            y: clampY(gradient.end.y * h),\n          },\n          showConnector: true,\n        };\n      }\n      const dir = angleToDir(gradient.angle);\n      // Inset the edge by half a handle so the dot stays fully inside the box\n      // and doesn't get clipped by `overflow-hidden` on the container. The\n      // gradient itself still paints to the true edge — only the handle\n      // visualization is inset.\n      const inset = HANDLE_PX / 2;\n      const t = edgeExtent(\n        dir,\n        Math.max(0, w / 2 - inset),\n        Math.max(0, h / 2 - inset),\n      );\n      return {\n        a: { x: cx - dir.x * t, y: cy - dir.y * t },\n        b: { x: cx + dir.x * t, y: cy + dir.y * t },\n        showConnector: true,\n      };\n    }\n\n    if (gradient.type === \"radial\") {\n      const ax = gradient.center.x * w;\n      const ay = gradient.center.y * h;\n      // Edge handle position resolves in three precedence tiers, matching\n      // formatGradient's emit logic:\n      //   1. circle + radiusPx → place the handle on the ring at (rPx, 0).\n      //      Single-axis since the gradient is pixel-circular.\n      //   2. radii (ellipse) → place the handle at the +x +y corner of the\n      //      ellipse bounding box.\n      //   3. keyword form → seed both from `keywordToRadii` and treat the\n      //      handle as ellipse-style for the visual until the user\n      //      promotes (drag promotes per shape).\n      let bx: number;\n      let by: number;\n      if (gradient.shape === \"circle\" && gradient.radiusPx !== undefined) {\n        bx = ax + gradient.radiusPx;\n        by = ay;\n      } else if (gradient.radii) {\n        bx = ax + gradient.radii.x * w;\n        by = ay + gradient.radii.y * h;\n      } else {\n        const seeded = keywordToRadii(\n          gradient.shape,\n          gradient.size,\n          gradient.center,\n          w,\n          h,\n        );\n        bx = ax + seeded.x * w;\n        by = ay + seeded.y * h;\n      }\n      return {\n        a: { x: ax, y: ay },\n        b: { x: bx, y: by } as XY,\n        showConnector: false,\n      };\n    }\n\n    // conic\n    const a = { x: gradient.center.x * w, y: gradient.center.y * h };\n    const dir = angleToDir(gradient.startAngle);\n    const dialR = conicDialRadius ?? Math.min(w, h) / 2 - HANDLE_PX;\n    return {\n      a,\n      b: { x: a.x + dir.x * dialR, y: a.y + dir.y * dialR },\n      showConnector: true,\n    };\n  }, [dims, gradient, conicDialRadius]);\n\n  // Drag helpers ------------------------------------------------------------\n\n  const localFromEvent = (clientX: number, clientY: number): XY => {\n    const el = padRef.current;\n    if (!el) return { x: 0, y: 0 };\n    const r = el.getBoundingClientRect();\n    return { x: clientX - r.left, y: clientY - r.top };\n  };\n\n  type HandleKind =\n    | \"linear-a\"\n    | \"linear-b\"\n    | \"center\"\n    | \"conic-dial\"\n    | \"radial-edge\";\n\n  const handleAt = (kind: HandleKind, p: XY, shiftKey: boolean) => {\n    const { w, h } = dims;\n    const cx = w / 2;\n    const cy = h / 2;\n\n    if (kind === \"linear-a\" || kind === \"linear-b\") {\n      if (gradient.type !== \"linear\") return;\n      // Free position: drag puts the endpoint exactly where the pointer is.\n      // Promote the gradient to \"positioned\" mode by ensuring both `start`\n      // and `end` are populated — seed the other endpoint from the angle if\n      // this is the first drag.\n      const nx = Math.max(0, Math.min(1, p.x / w));\n      const ny = Math.max(0, Math.min(1, p.y / h));\n      const ensureOther = (existing: { x: number; y: number } | undefined) => {\n        if (existing) return existing;\n        // Seed from the current angle so the line keeps its visual direction\n        // for the very first drag — no visible jump on promotion.\n        const dir = angleToDir(gradient.angle);\n        const inset = HANDLE_PX / 2;\n        const t = edgeExtent(\n          dir,\n          Math.max(0, w / 2 - inset),\n          Math.max(0, h / 2 - inset),\n        );\n        // The \"other\" endpoint is opposite the one being dragged.\n        const sign = kind === \"linear-a\" ? 1 : -1;\n        const ox = cx + dir.x * t * sign;\n        const oy = cy + dir.y * t * sign;\n        return { x: ox / w, y: oy / h };\n      };\n      if (kind === \"linear-a\") {\n        const other = ensureOther(gradient.end);\n        if (!gradient.end) ctx.setLinearEnd(other);\n        ctx.setLinearStart({ x: nx, y: ny });\n      } else {\n        const other = ensureOther(gradient.start);\n        if (!gradient.start) ctx.setLinearStart(other);\n        ctx.setLinearEnd({ x: nx, y: ny });\n      }\n      return;\n    }\n\n    if (kind === \"center\") {\n      const nx = Math.max(0, Math.min(1, p.x / w));\n      const ny = Math.max(0, Math.min(1, p.y / h));\n      ctx.setCenter({ x: nx, y: ny });\n      return;\n    }\n\n    if (kind === \"radial-edge\") {\n      if (gradient.type !== \"radial\") return;\n      const ax = gradient.center.x * w;\n      const ay = gradient.center.y * h;\n      // Edge handle lives in the +x +y quadrant relative to the center —\n      // drags into other quadrants are mirrored via abs() so radii stay\n      // strictly non-negative (CSS forbids negative gradient radii).\n      const dxPx = Math.abs(p.x - ax);\n      const dyPx = Math.abs(p.y - ay);\n      // Honor the declared shape, Shift inverts (and flips shape in the\n      // model — `setRadiusPx` / `setRadii` enforce `shape: \"circle\"` /\n      // `\"ellipse\"` respectively, so the data, the CSS output, and the\n      // ShapeSwitcher all stay in sync after a Shift-drag).\n      //   shape=circle + no shift  → stays circle, writes radiusPx\n      //   shape=circle + shift     → flips to ellipse, writes radii\n      //   shape=ellipse + no shift → stays ellipse, writes radii\n      //   shape=ellipse + shift    → flips to circle, writes radiusPx\n      const renderAsCircle = (gradient.shape === \"circle\") !== shiftKey;\n      if (renderAsCircle) {\n        // True pixel circle: pick the larger pointer distance as the\n        // radius. Stored in absolute pixels so the consumer-side CSS\n        // (`circle <px>px`) renders a real circle in any container.\n        const rPx = Math.max(dxPx, dyPx);\n        ctx.setRadiusPx(rPx);\n      } else {\n        ctx.setRadii({\n          x: Math.max(0, Math.min(2, dxPx / w)),\n          y: Math.max(0, Math.min(2, dyPx / h)),\n        });\n      }\n      return;\n    }\n\n    // conic-dial — rotate around current center, distance is ignored.\n    if (gradient.type !== \"conic\") return;\n    const ax = gradient.center.x * w;\n    const ay = gradient.center.y * h;\n    let deg = dirToAngle(p.x - ax, p.y - ay);\n    if (shiftKey) deg = snapDeg(deg, 15);\n    ctx.setStartAngle(deg);\n  };\n\n  // Keyboard nudge helpers --------------------------------------------------\n\n  const rotate = (current: number, e: React.KeyboardEvent): number | null => {\n    const step = e.shiftKey ? 15 : 1;\n    let next = current;\n    if (e.key === \"ArrowRight\" || e.key === \"ArrowUp\") next = current + step;\n    else if (e.key === \"ArrowLeft\" || e.key === \"ArrowDown\")\n      next = current - step;\n    else if (e.key === \"Home\") next = 0;\n    else if (e.key === \"End\") next = 359;\n    else return null;\n    return ((next % 360) + 360) % 360;\n  };\n\n  // 2D handles (center, free-positioned endpoints) use role=\"application\",\n  // which has no value semantics — announce keyboard moves in a polite live\n  // region rendered at the overlay root.\n  const [liveText, announce] = useLiveAnnounce();\n\n  /**\n   * Stack three background layers so a handle dot shows the stop's actual\n   * color (alpha intact) while still fully occluding the dashed gradient\n   * line behind it: stop-color on top, checkerboard underneath so\n   * transparency reads as transparency, and the Handle's own solid\n   * `bg-background` at the bottom for the final opacity guarantee.\n   */\n  const stopSwatchStyle = (color: typeof ctx.stops[number][\"color\"]) => {\n    const css = formatColor(color, \"oklch\");\n    return {\n      backgroundImage: `linear-gradient(${css}, ${css}), ${CHECKERBOARD_LG}`,\n      backgroundSize: \"auto, 6px 6px\",\n    } as const;\n  };\n\n  const onKeyDownAngle = (e: React.KeyboardEvent<HTMLButtonElement>) => {\n    if (gradient.type !== \"linear\") return;\n    const next = rotate(gradient.angle, e);\n    if (next === null) return;\n    e.preventDefault();\n    ctx.setAngle(next);\n  };\n\n  const onKeyDownLinearEndpoint = (\n    which: \"linear-a\" | \"linear-b\",\n    e: React.KeyboardEvent<HTMLButtonElement>,\n  ) => {\n    if (gradient.type !== \"linear\") return;\n    const point =\n      which === \"linear-a\" ? gradient.start : gradient.end;\n    if (!point) {\n      // Fall back to angle rotation when this handle hasn't been promoted\n      // to free-position mode yet — keeps the angle-only behavior intact.\n      onKeyDownAngle(e);\n      return;\n    }\n    const step = e.shiftKey ? 0.05 : 0.01;\n    let { x, y } = point;\n    if (e.key === \"ArrowLeft\") x -= step;\n    else if (e.key === \"ArrowRight\") x += step;\n    else if (e.key === \"ArrowUp\") y -= step;\n    else if (e.key === \"ArrowDown\") y += step;\n    else return;\n    e.preventDefault();\n    const next = {\n      x: Math.max(0, Math.min(1, x)),\n      y: Math.max(0, Math.min(1, y)),\n    };\n    if (which === \"linear-a\") ctx.setLinearStart(next);\n    else ctx.setLinearEnd(next);\n    announce(\n      `${which === \"linear-a\" ? \"Start\" : \"End\"} x ${Math.round(next.x * 100)}%, y ${Math.round(next.y * 100)}%`,\n    );\n  };\n\n  const onKeyDownConicDial = (e: React.KeyboardEvent<HTMLButtonElement>) => {\n    if (gradient.type !== \"conic\") return;\n    const next = rotate(gradient.startAngle, e);\n    if (next === null) return;\n    e.preventDefault();\n    ctx.setStartAngle(next);\n  };\n\n  const onKeyDownRadii = (e: React.KeyboardEvent<HTMLButtonElement>) => {\n    if (gradient.type !== \"radial\") return;\n    const { w, h } = dims;\n    if (w === 0 || h === 0) return;\n    // Same shape-lock convention as the pointer drag.\n    const renderAsCircle = (gradient.shape === \"circle\") !== e.shiftKey;\n    const stepRatio = e.shiftKey ? 0.05 : 0.01;\n\n    if (renderAsCircle) {\n      // Resolve the current radius in pixels regardless of which field is\n      // populated, then nudge in pixel space.\n      const currentPx =\n        gradient.radiusPx ??\n        (gradient.radii\n          ? Math.max(gradient.radii.x * w, gradient.radii.y * h)\n          : keywordToRadii(\n              gradient.shape,\n              gradient.size,\n              gradient.center,\n              w,\n              h,\n            ).x * w);\n      const stepPx = stepRatio * Math.min(w, h);\n      let next = currentPx;\n      if (e.key === \"ArrowLeft\" || e.key === \"ArrowDown\") next -= stepPx;\n      else if (e.key === \"ArrowRight\" || e.key === \"ArrowUp\") next += stepPx;\n      else return;\n      e.preventDefault();\n      ctx.setRadiusPx(Math.max(0, next));\n      return;\n    }\n\n    // Ellipse path: per-axis nudge in normalized space, same as before.\n    const current =\n      gradient.radii ??\n      keywordToRadii(gradient.shape, gradient.size, gradient.center, w, h);\n    let { x, y } = current;\n    if (e.key === \"ArrowLeft\") x -= stepRatio;\n    else if (e.key === \"ArrowRight\") x += stepRatio;\n    else if (e.key === \"ArrowUp\") y -= stepRatio;\n    else if (e.key === \"ArrowDown\") y += stepRatio;\n    else return;\n    e.preventDefault();\n    ctx.setRadii({\n      x: Math.max(0, Math.min(2, x)),\n      y: Math.max(0, Math.min(2, y)),\n    });\n  };\n\n  const onKeyDownCenter = (e: React.KeyboardEvent<HTMLButtonElement>) => {\n    if (gradient.type === \"linear\") return;\n    const center = gradient.center;\n    const step = e.shiftKey ? 0.05 : 0.01;\n    let { x, y } = center;\n    if (e.key === \"ArrowLeft\") x -= step;\n    else if (e.key === \"ArrowRight\") x += step;\n    else if (e.key === \"ArrowUp\") y -= step;\n    else if (e.key === \"ArrowDown\") y += step;\n    else if (e.key === \"Home\") {\n      x = 0.5;\n      y = 0.5;\n    } else return;\n    e.preventDefault();\n    const next = {\n      x: Math.max(0, Math.min(1, x)),\n      y: Math.max(0, Math.min(1, y)),\n    };\n    ctx.setCenter(next);\n    announce(\n      `Center x ${Math.round(next.x * 100)}%, y ${Math.round(next.y * 100)}%`,\n    );\n  };\n\n  const beginDrag = (\n    kind: HandleKind,\n    e: React.PointerEvent<HTMLButtonElement>,\n  ) => {\n    e.preventDefault();\n    e.stopPropagation();\n    handleAt(kind, localFromEvent(e.clientX, e.clientY), e.shiftKey);\n    trackPointerDrag(e.currentTarget, e.pointerId, (ev) =>\n      handleAt(kind, localFromEvent(ev.clientX, ev.clientY), ev.shiftKey),\n    );\n  };\n\n  // Radius handle ARIA. The pixel radius of a circle is unbounded (it can\n  // exceed the box), and keyword circles normalized to width can pass 200%\n  // on tall boxes — so the max grows with the value instead of letting\n  // aria-valuenow overshoot a fixed aria-valuemax.\n  const radiusAria = (() => {\n    if (gradient.type !== \"radial\" || dims.w === 0 || dims.h === 0) return null;\n    if (gradient.shape === \"circle\" && gradient.radiusPx !== undefined) {\n      const now = Math.round(gradient.radiusPx);\n      return {\n        now,\n        max: Math.max(now, Math.round(Math.hypot(dims.w, dims.h))),\n        text: `circle radius ${now} pixels`,\n      };\n    }\n    const r =\n      gradient.radii ??\n      keywordToRadii(\n        gradient.shape,\n        gradient.size,\n        gradient.center,\n        dims.w,\n        dims.h,\n      );\n    const now = Math.round(r.x * 100);\n    return {\n      now,\n      max: Math.max(200, now),\n      text: `radius x ${now}%, y ${Math.round(r.y * 100)}%`,\n    };\n  })();\n\n  // Render ------------------------------------------------------------------\n\n  return (\n    <div\n      ref={padRef}\n      data-slot=\"gradient-overlay\"\n      style={style}\n      className={cn(\n        // Fills the host container; pointer-events:none so empty regions\n        // fall through to whatever sits beneath (consumer's canvas, the\n        // <Area>'s painted bg, etc.). The handle buttons explicitly set\n        // pointer-events:auto so they still receive clicks.\n        \"pointer-events-none absolute inset-0\",\n        className,\n      )}\n      {...rest}\n    >\n      <span aria-live=\"polite\" className=\"sr-only\">\n        {liveText}\n      </span>\n      {handles && (\n        <>\n          {handles.showConnector && handles.b && (\n            <svg\n              aria-hidden\n              className=\"pointer-events-none absolute inset-0\"\n              width={dims.w}\n              height={dims.h}\n            >\n              <line\n                x1={handles.a.x}\n                y1={handles.a.y}\n                x2={handles.b.x}\n                y2={handles.b.y}\n                stroke=\"white\"\n                strokeWidth={1.5}\n                strokeDasharray=\"4 4\"\n                strokeOpacity={0.85}\n                style={{\n                  filter: \"drop-shadow(0 0 1px rgba(0,0,0,0.55))\",\n                }}\n              />\n            </svg>\n          )}\n\n          {gradient.type === \"radial\" && handles.b && (\n            <svg\n              aria-hidden\n              className=\"pointer-events-none absolute inset-0\"\n              width={dims.w}\n              height={dims.h}\n            >\n              {(() => {\n                const ax = handles.a.x;\n                const ay = handles.a.y;\n                const rx = Math.abs(handles.b.x - ax);\n                const ry = Math.abs(handles.b.y - ay);\n                // shape=\"circle\" always renders a true circle outline so\n                // radiusPx-circles (where the edge handle sits on the x\n                // axis and ry === 0) don't collapse to a flat line. For\n                // the keyword-circle path, rx === ry already.\n                const isCircle = gradient.shape === \"circle\";\n                const stroke = {\n                  fill: \"none\",\n                  stroke: \"white\",\n                  strokeWidth: 1.5,\n                  strokeDasharray: \"4 4\",\n                  strokeOpacity: 0.85,\n                  style: { filter: \"drop-shadow(0 0 1px rgba(0,0,0,0.55))\" },\n                } as const;\n                return isCircle ? (\n                  <circle cx={ax} cy={ay} r={Math.max(rx, ry)} {...stroke} />\n                ) : (\n                  <ellipse cx={ax} cy={ay} rx={rx} ry={ry} {...stroke} />\n                );\n              })()}\n            </svg>\n          )}\n\n          {gradient.type === \"linear\" ? (\n            <>\n              {/* Free-positioned endpoints move in 2D (x/y), so they must\n                 not claim role=\"slider\" — its 1D value model would keep\n                 announcing the angle while arrows change position. They\n                 become labelled 2D pads like the center handle; angle-only\n                 mode keeps the true slider semantics. */}\n              <Handle\n                label={\n                  gradient.start\n                    ? `Gradient start, x ${Math.round(gradient.start.x * 100)}%, y ${Math.round(gradient.start.y * 100)}%`\n                    : \"Gradient start\"\n                }\n                position={handles.a}\n                onPointerDown={(event) => beginDrag(\"linear-a\", event)}\n                onKeyDown={(e) => onKeyDownLinearEndpoint(\"linear-a\", e)}\n                {...(gradient.start\n                  ? {\n                      role: \"application\" as const,\n                      \"aria-roledescription\": \"2D pad for gradient endpoint\",\n                    }\n                  : {\n                      role: \"slider\" as const,\n                      \"aria-valuemin\": 0,\n                      \"aria-valuemax\": 360,\n                      \"aria-valuenow\": Math.round(gradient.angle),\n                      \"aria-valuetext\": `${Math.round(gradient.angle)} degrees`,\n                    })}\n                // Endpoint handles are always anchored to the first / last\n                // stop, so they render in that stop's color.\n                style={\n                  ctx.stops[0] ? stopSwatchStyle(ctx.stops[0].color) : undefined\n                }\n              />\n              {handles.b && (\n                <Handle\n                  label={\n                    gradient.end\n                      ? `Gradient end, x ${Math.round(gradient.end.x * 100)}%, y ${Math.round(gradient.end.y * 100)}%`\n                      : \"Gradient end\"\n                  }\n                  position={handles.b}\n                  onPointerDown={(event) => beginDrag(\"linear-b\", event)}\n                  onKeyDown={(e) => onKeyDownLinearEndpoint(\"linear-b\", e)}\n                  {...(gradient.end\n                    ? {\n                        role: \"application\" as const,\n                        \"aria-roledescription\": \"2D pad for gradient endpoint\",\n                      }\n                    : {\n                        role: \"slider\" as const,\n                        \"aria-valuemin\": 0,\n                        \"aria-valuemax\": 360,\n                        \"aria-valuenow\": Math.round(gradient.angle),\n                        \"aria-valuetext\": `${Math.round(gradient.angle)} degrees`,\n                      })}\n                  style={(() => {\n                    const last = ctx.stops[ctx.stops.length - 1];\n                    return last ? stopSwatchStyle(last.color) : undefined;\n                  })()}\n                />\n              )}\n              {/* Middle stops are intentionally NOT rendered on the canvas:\n                 the Bar is the editing surface for inner stops. The overlay\n                 surfaces only the gradient *geometry* — the two endpoints\n                 (tinted with the first / last stop colors) and the line. */}\n            </>\n          ) : (\n            <>\n              <Handle\n                label={`Gradient center, x ${Math.round(gradient.center.x * 100)}%, y ${Math.round(gradient.center.y * 100)}%`}\n                position={handles.a}\n                onPointerDown={(event) => beginDrag(\"center\", event)}\n                onKeyDown={onKeyDownCenter}\n                role=\"application\"\n                aria-roledescription=\"2D pad for gradient center\"\n              />\n              {handles.b && gradient.type === \"conic\" && (\n                <Handle\n                  label=\"Gradient start angle\"\n                  position={handles.b}\n                  onPointerDown={(event) => beginDrag(\"conic-dial\", event)}\n                  onKeyDown={onKeyDownConicDial}\n                  role=\"slider\"\n                  aria-valuemin={0}\n                  aria-valuemax={360}\n                  aria-valuenow={Math.round(gradient.startAngle)}\n                  aria-valuetext={`${Math.round(gradient.startAngle)} degrees`}\n                />\n              )}\n              {handles.b && radiusAria && (\n                <Handle\n                  label=\"Gradient radius\"\n                  position={handles.b}\n                  onPointerDown={(event) => beginDrag(\"radial-edge\", event)}\n                  onKeyDown={onKeyDownRadii}\n                  role=\"slider\"\n                  aria-valuemin={0}\n                  aria-valuemax={radiusAria.max}\n                  aria-valuenow={radiusAria.now}\n                  aria-valuetext={radiusAria.text}\n                />\n              )}\n            </>\n          )}\n        </>\n      )}\n    </div>\n  );\n});\n\ninterface HandleProps\n  extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, \"aria-label\"> {\n  label: string;\n  position: XY;\n}\n\nfunction Handle({ label, position, className, style, ...rest }: HandleProps) {\n  return (\n    <button\n      type=\"button\"\n      aria-label={label}\n      tabIndex={0}\n      style={{\n        left: position.x,\n        top: position.y,\n        width: HANDLE_PX,\n        height: HANDLE_PX,\n        ...style,\n      }}\n      className={cn(\n        \"absolute -translate-x-1/2 -translate-y-1/2 cursor-grab touch-none rounded-full\",\n        // WCAG 2.5.8: 14px visual dot, ≥24px pointer target.\n        \"before:absolute before:-inset-1.5 before:content-['']\",\n        // Solid background so the dashed gradient line behind the dot is\n        // fully hidden by it (a translucent fill would let the line bleed\n        // through visibly). `pointer-events-auto` so the buttons keep\n        // receiving clicks even when their parent overlay is set to\n        // `pointer-events-none` (the default for `<GradientPicker.Overlay>`).\n        \"pointer-events-auto border-2 border-white bg-background shadow-[0_0_0_1px_rgba(0,0,0,0.55),0_2px_6px_rgba(0,0,0,0.35)]\",\n        \"outline-none motion-safe:transition-transform\",\n        \"motion-safe:hover:scale-110 active:cursor-grabbing motion-safe:active:scale-95\",\n        \"focus-visible:ring-2 focus-visible:ring-ring\",\n        className,\n      )}\n      {...rest}\n    />\n  );\n}\n"
    },
    {
      "path": "registry/new-york/ui/fill-picker/parts/gradient/area.tsx",
      "type": "registry:ui",
      "target": "@ui/fill-picker/parts/gradient/area.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { useGradientPickerContext } from \"../../contexts/gradient\";\nimport { formatGradient } from \"../../lib/gradient\";\nimport { CHECKERBOARD_LG } from \"../../lib/constants\";\nimport { Overlay, type OverlayProps } from \"./overlay\";\n\nexport interface AreaProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** Fixed height in px. Defaults to 120. Width always fills the container. */\n  height?: number;\n  /** Forwarded to the inner `<GradientPicker.Overlay>` for conic dial sizing. */\n  conicDialRadius?: OverlayProps[\"conicDialRadius\"];\n}\n\n/**\n * Self-contained 2D pad: paints the live gradient (over a transparency\n * checkerboard) and overlays the handle layer on top. Essentially\n * `<GradientPicker.Overlay>` plus a painted background — use this when you\n * want the picker to own its own preview, and use `<GradientPicker.Overlay>`\n * directly when you want to drop the handles onto a consumer-owned object\n * on a canvas.\n */\nexport const Area = React.forwardRef<HTMLDivElement, AreaProps>(function Area(\n  { className, height = 120, conicDialRadius, style, ...rest },\n  ref,\n) {\n  const ctx = useGradientPickerContext();\n  const cssBackground = React.useMemo(\n    () => formatGradient(ctx.gradient),\n    [ctx.gradient],\n  );\n\n  return (\n    <div\n      ref={ref}\n      data-slot=\"gradient-area\"\n      style={{ height, ...style }}\n      className={cn(\n        \"relative w-full overflow-hidden rounded-md border border-border bg-muted\",\n        className,\n      )}\n      {...rest}\n    >\n      {/* Transparency checker behind the gradient so alpha is legible. */}\n      <div\n        aria-hidden\n        className=\"absolute inset-0\"\n        style={{ backgroundImage: CHECKERBOARD_LG }}\n      />\n      <div\n        aria-hidden\n        className=\"absolute inset-0\"\n        style={{ background: cssBackground }}\n      />\n      <Overlay conicDialRadius={conicDialRadius} />\n    </div>\n  );\n});\n"
    },
    {
      "path": "registry/new-york/ui/fill-picker/parts/gradient/pointer-drag.ts",
      "type": "registry:ui",
      "target": "@ui/fill-picker/parts/gradient/pointer-drag.ts",
      "content": "\"use client\";\n\n/**\n * Capture-based drag loop shared by the overlay handles and the angle /\n * position pads. Besides the usual pointerup/pointercancel cleanup it\n * defends against the two ways a drag gets \"stuck\" chasing the cursor:\n *\n *  - a move arriving with no buttons pressed (the release happened where\n *    we couldn't see it — e.g. capture silently dropped, button released\n *    during an OS gesture) ends the drag instead of dragging on;\n *  - `lostpointercapture` (window blur / cmd-tab mid-drag revokes capture\n *    without any pointerup) tears the listeners down immediately.\n */\nexport function trackPointerDrag(\n  target: HTMLElement,\n  pointerId: number,\n  onMove: (ev: PointerEvent) => void,\n): void {\n  try {\n    target.setPointerCapture(pointerId);\n  } catch {\n    // inactive pointer id — the drag still works while the pointer stays\n    // over the target; the buttons check below handles missed releases.\n  }\n  const move = (ev: PointerEvent) => {\n    // Only the dragging pointer steers or ends the gesture — a second\n    // touch point wandering over the captured element must not.\n    if (ev.pointerId !== pointerId) return;\n    if (ev.buttons === 0) {\n      end(ev);\n      return;\n    }\n    onMove(ev);\n  };\n  const end = (ev?: Event) => {\n    if (ev instanceof PointerEvent && ev.pointerId !== pointerId) return;\n    if (ev instanceof PointerEvent) {\n      try {\n        target.releasePointerCapture(ev.pointerId);\n      } catch {\n        // pointer may already be released on cancel\n      }\n    }\n    target.removeEventListener(\"pointermove\", move);\n    target.removeEventListener(\"pointerup\", end);\n    target.removeEventListener(\"pointercancel\", end);\n    target.removeEventListener(\"lostpointercapture\", end);\n  };\n  target.addEventListener(\"pointermove\", move);\n  target.addEventListener(\"pointerup\", end);\n  target.addEventListener(\"pointercancel\", end);\n  target.addEventListener(\"lostpointercapture\", end);\n}\n"
    },
    {
      "path": "registry/new-york/ui/fill-picker/parts/gradient/stop-list.tsx",
      "type": "registry:ui",
      "target": "@ui/fill-picker/parts/gradient/stop-list.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Plus, Minus } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  useGradientPickerContext,\n  useGradientStopEditor,\n} from \"../../contexts/gradient\";\nimport {\n  projectStopPosition,\n  reverseProjectStopPosition,\n} from \"../../lib/gradient\";\nimport {\n  focusNeighborOption,\n  insertStopAfterSelected,\n  isEventFromRowControl,\n  stopListKeyNav,\n} from \"./stop-list-shared\";\nimport { formatColor, parseColor } from \"../../lib/color\";\nimport { CHECKERBOARD_SM } from \"../../lib/constants\";\nimport {\n  FieldDraftInput,\n  FieldInput,\n  FieldInputGroup,\n  FieldShell,\n  FieldSuffix,\n} from \"../field\";\n\nexport interface StopListProps extends React.HTMLAttributes<HTMLDivElement> {\n  /**\n   * Render a trailing \"+ Add stop\" row that inserts a new stop in the\n   * largest gap between adjacent stops, sampling the gradient's existing\n   * ramp at that position so the new color blends in. Defaults to `true`.\n   * Set `false` to hide it when `<GradientPicker.Bar>` (the canonical\n   * add-stop UI) is visible and the trailing row would be redundant.\n   */\n  showAddStop?: boolean;\n}\n\nexport const StopList = React.forwardRef<HTMLDivElement, StopListProps>(\n  function StopList(\n    { className, showAddStop = true, ...rest },\n    ref,\n  ) {\n  const ctx = useGradientPickerContext();\n  // Each row mounts its own copy of the injected stop editor (bound to that\n  // row's stop) so opening any popover edits the right stop directly — no\n  // need for a list-wide ColorPicker context.\n  // Mirror the Bar's projection: when the gradient is a positioned linear,\n  // show + edit the *visible* position so this list matches what the user\n  // sees in the Area and Bar. Authored positions still live in 0..1 of the\n  // [start, end] segment; we convert at the edges.\n  const linear = ctx.gradient.type === \"linear\" ? ctx.gradient : null;\n  const start = linear?.start;\n  const end = linear?.end;\n  const toDisplay = (authored: number) =>\n    projectStopPosition(authored, start, end);\n  const fromDisplay = (displayed: number) =>\n    reverseProjectStopPosition(displayed, start, end);\n\n  const handleAddStop = () => {\n    const { position, color } = insertStopAfterSelected(\n      ctx.stops,\n      ctx.selectedStopId,\n    );\n    const id = ctx.addStop(position, color);\n    ctx.selectStop(id);\n  };\n  return (\n    <div\n      ref={ref}\n      data-slot=\"gradient-stop-list\"\n      role=\"listbox\"\n      aria-label=\"Gradient stops\"\n      onKeyDown={stopListKeyNav}\n      className={cn(\"flex flex-col gap-1\", className)}\n      {...rest}\n    >\n      {ctx.stops.map((s) => {\n        const selected = s.id === ctx.selectedStopId;\n        return (\n          <StopRow\n            key={s.id}\n            stop={s}\n            selected={selected}\n            toDisplay={toDisplay}\n            fromDisplay={fromDisplay}\n            formatted={formatColor(s.color, ctx.getStopColorFormat(s.id))}\n          />\n        );\n      })}\n      {showAddStop && (\n        <Button\n          type=\"button\"\n          variant=\"outline\"\n          onClick={handleAddStop}\n          aria-label=\"Add stop\"\n          className=\"h-8 cursor-pointer font-mono text-xs tracking-wide shadow-xs\"\n        >\n          <Plus aria-hidden className=\"size-3.5\" />\n          Add stop\n        </Button>\n      )}\n    </div>\n  );\n});\n\nfunction StopRow({\n  stop: s,\n  selected,\n  toDisplay,\n  fromDisplay,\n  formatted,\n}: {\n  stop: ReturnType<typeof useGradientPickerContext>[\"stops\"][number];\n  selected: boolean;\n  toDisplay: (n: number) => number;\n  fromDisplay: (n: number) => number;\n  formatted: string;\n}) {\n  const ctx = useGradientPickerContext();\n  // Same slot the Bar uses: the editor's UI dialect ships per variant and is\n  // injected by the barrel, so this list stays importable from either tree\n  // without dragging one dialect's primitives in.\n  const stopEditor = useGradientStopEditor();\n  const [open, setOpen] = React.useState(false);\n  const [draft, setDraft] = React.useState(formatted);\n  const focusedRef = React.useRef(false);\n  // Escape reverts by blurring, and that blur would otherwise commit the\n  // still-typed DOM value — the reverted draft hasn't rendered yet.\n  const cancelBlurCommitRef = React.useRef(false);\n  React.useEffect(() => {\n    if (!focusedRef.current) setDraft(formatted);\n  }, [formatted]);\n  const commitDraft = (raw: string) => {\n    const parsed = parseColor(raw.trim());\n    if (parsed) ctx.setStopColor(s.id, parsed);\n    else setDraft(formatted);\n  };\n  const swatch = (\n    <button\n      type=\"button\"\n      onClick={(e) => {\n        e.stopPropagation();\n        ctx.selectStop(s.id);\n        setOpen((o) => !o);\n      }}\n      aria-label=\"Edit stop color\"\n      style={{\n        backgroundImage: `linear-gradient(${formatColor(s.color, \"oklch\")}, ${formatColor(s.color, \"oklch\")}), ${CHECKERBOARD_SM}`,\n        backgroundSize: \"auto, 6px 6px\",\n      }}\n      className=\"size-7 shrink-0 rounded-xs border border-border outline-none transition-shadow hover:ring-2 hover:ring-ring focus-visible:ring-2 focus-visible:ring-ring\"\n    />\n  );\n  return (\n    <div\n      role=\"option\"\n      aria-selected={selected}\n      tabIndex={selected ? 0 : -1}\n      data-selected={selected}\n      onClick={() => ctx.selectStop(s.id)}\n      onKeyDown={(e) => {\n        // Only when the option row itself is focused — keys typed into the\n        // nested inputs (e.g. Backspace while editing the color text) must\n        // never trigger row shortcuts.\n        if (isEventFromRowControl(e)) return;\n        if (e.key === \"Enter\" || e.key === \" \") {\n          e.preventDefault();\n          ctx.selectStop(s.id);\n        } else if (e.key === \"Delete\" || e.key === \"Backspace\") {\n          e.preventDefault();\n          // Focus the neighbor before removal so focus stays in the list.\n          focusNeighborOption(e.currentTarget);\n          ctx.removeStop(s.id);\n        }\n      }}\n      className={cn(\n        \"flex cursor-pointer items-center gap-2 rounded-md border p-1 text-xs outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n        selected ? \"border-foreground\" : \"border-border\",\n      )}\n    >\n      {stopEditor\n        ? stopEditor({\n            stopId: s.id,\n            open,\n            onOpenChange: setOpen,\n            children: swatch,\n          })\n        : swatch}\n      <FieldShell className=\"h-7 w-fit\">\n        <FieldInputGroup>\n          <span className=\"sr-only\">Stop position</span>\n          <FieldDraftInput\n            inputMode=\"decimal\"\n            nudge={1}\n            value={String(Math.round(toDisplay(s.position) * 100))}\n            onCommit={(raw) => {\n              const v = parseFloat(raw);\n              // Clamp the typed *displayed* percent to the track; moveStop\n              // itself no longer clamps (extrapolated stops are legal).\n              if (Number.isFinite(v))\n                ctx.moveStop(\n                  s.id,\n                  fromDisplay(Math.max(0, Math.min(1, v / 100))),\n                );\n            }}\n            aria-label=\"Stop position\"\n            className=\"w-10\"\n          />\n          <FieldSuffix>%</FieldSuffix>\n        </FieldInputGroup>\n      </FieldShell>\n      <FieldShell className=\"h-7 min-w-0 flex-1\">\n        <FieldInputGroup>\n          <span className=\"sr-only\">Stop color (paste hex / css)</span>\n          <FieldInput\n            value={draft}\n            spellCheck={false}\n            onFocus={() => {\n              focusedRef.current = true;\n            }}\n            onClick={(e) => e.stopPropagation()}\n            onChange={(e) => setDraft(e.target.value)}\n            onBlur={(e) => {\n              focusedRef.current = false;\n              if (cancelBlurCommitRef.current) {\n                cancelBlurCommitRef.current = false;\n                return;\n              }\n              commitDraft(e.target.value);\n            }}\n            onKeyDown={(e) => {\n              if (e.key === \"Enter\") {\n                e.preventDefault();\n                commitDraft((e.target as HTMLInputElement).value);\n                (e.target as HTMLInputElement).blur();\n              } else if (e.key === \"Escape\") {\n                e.preventDefault();\n                setDraft(formatted);\n                cancelBlurCommitRef.current = true;\n                (e.target as HTMLInputElement).blur();\n              }\n            }}\n            aria-label=\"Stop color value\"\n            className=\"text-left\"\n          />\n        </FieldInputGroup>\n      </FieldShell>\n      <button\n        type=\"button\"\n        onClick={(e) => {\n          e.stopPropagation();\n          const row = e.currentTarget.closest<HTMLElement>('[role=\"option\"]');\n          if (row) focusNeighborOption(row);\n          ctx.removeStop(s.id);\n        }}\n        disabled={ctx.stops.length <= 1}\n        className=\"inline-flex size-7 items-center justify-center rounded-md border border-input text-muted-foreground shadow-xs hover:text-foreground disabled:opacity-30\"\n        aria-label=\"Remove stop\"\n      >\n        <Minus className=\"size-3.5\" />\n      </button>\n    </div>\n  );\n}\n"
    },
    {
      "path": "registry/new-york/ui/fill-picker/parts/gradient/stop-list-shared.ts",
      "type": "registry:ui",
      "target": "@ui/fill-picker/parts/gradient/stop-list-shared.ts",
      "content": "\"use client\";\n\n/**\n * Behavior shared by the two StopList variants (classic and Base UI) so\n * keyboard/list logic can't drift between them: listbox arrow navigation,\n * focus retention on delete, and the insert-after-selected placement math.\n */\n\nimport * as React from \"react\";\nimport { sampleStopsAt } from \"../../lib/gradient\";\nimport type { OklchColor } from \"../../lib/types\";\n\n/**\n * True when the key event originated inside a form control nested in the\n * option row (position/color inputs, popover trigger, remove button) rather\n * than on the option itself. List/row shortcuts must ignore those — e.g.\n * Backspace while editing the color text must edit text, not delete the\n * stop; ArrowUp/Down must nudge the numeric field, not move list focus.\n */\nexport function isEventFromRowControl(\n  e: React.KeyboardEvent<HTMLElement>,\n): boolean {\n  return e.target !== e.currentTarget;\n}\n\n/**\n * Listbox keyboard navigation for the stop list container: ArrowUp/Down\n * move to the neighboring option, Home/End jump to the edges. Selection\n * follows focus — activation goes through `.click()` so the row's own\n * onClick (selectStop) runs.\n */\nexport function stopListKeyNav(e: React.KeyboardEvent<HTMLElement>): void {\n  if (![\"ArrowDown\", \"ArrowUp\", \"Home\", \"End\"].includes(e.key)) return;\n  // Only navigate when the key originated on an option row itself. Keys\n  // from nested controls (position/color inputs — where ArrowUp/Down mean\n  // \"nudge the value\") must never be stolen by list navigation.\n  if (!isOption(e.target)) return;\n  const options = Array.from(\n    e.currentTarget.querySelectorAll<HTMLElement>('[role=\"option\"]'),\n  );\n  if (options.length === 0) return;\n  const current = options.indexOf(document.activeElement as HTMLElement);\n  let next: number;\n  if (e.key === \"Home\") next = 0;\n  else if (e.key === \"End\") next = options.length - 1;\n  else {\n    const delta = e.key === \"ArrowDown\" ? 1 : -1;\n    next = Math.max(0, Math.min(Math.max(current, 0) + delta, options.length - 1));\n  }\n  e.preventDefault();\n  options[next].focus();\n  options[next].click();\n}\n\nfunction isOption(target: EventTarget | null): boolean {\n  return (\n    target instanceof HTMLElement && target.getAttribute(\"role\") === \"option\"\n  );\n}\n\n/**\n * Move focus to the row's nearest option sibling. Call *before* removing\n * the stop — the neighbor's DOM node survives the re-render, so focus\n * stays in the list instead of being ejected to <body>.\n */\nexport function focusNeighborOption(row: HTMLElement): void {\n  const neighbor =\n    findOptionSibling(row, \"nextElementSibling\") ??\n    findOptionSibling(row, \"previousElementSibling\");\n  neighbor?.focus();\n}\n\n/**\n * Same intent as `focusNeighborOption`, but scans a container by selector\n * instead of walking siblings — the `<GradientPicker.Bar>` handles are\n * `role=\"slider\"` elements that may each sit inside a popover anchor wrapper,\n * so they are not reliably DOM siblings of one another.\n *\n * Call *before* removing the focused element. Returns true when focus moved.\n */\nexport function focusNeighborIn(\n  container: HTMLElement | null,\n  current: HTMLElement,\n  selector: string,\n): boolean {\n  if (!container) return false;\n  const items = Array.from(container.querySelectorAll<HTMLElement>(selector));\n  const idx = items.indexOf(current);\n  if (idx === -1) return false;\n  const neighbor = items[idx + 1] ?? items[idx - 1];\n  if (!neighbor) return false;\n  neighbor.focus();\n  return true;\n}\n\nfunction findOptionSibling(\n  el: HTMLElement,\n  dir: \"nextElementSibling\" | \"previousElementSibling\",\n): HTMLElement | null {\n  let node = el[dir];\n  while (node) {\n    if (node instanceof HTMLElement && node.getAttribute(\"role\") === \"option\") {\n      return node;\n    }\n    node = node[dir];\n  }\n  return null;\n}\n\n/**\n * Placement for the \"+ Add stop\" row: insert immediately after the selected\n * stop — halfway to its next neighbor, or halfway back to the previous one\n * when the selected stop is last (so a quick add lands somewhere visible\n * instead of squeezing into a near-zero end gap). Samples the existing ramp\n * so the inserted color blends in.\n */\nexport function insertStopAfterSelected<\n  T extends { id: string; position: number; color: OklchColor },\n>(stops: readonly T[], selectedStopId: string | null): {\n  position: number;\n  color: OklchColor;\n} {\n  const sorted = [...stops].sort((a, b) => a.position - b.position);\n  // A stop-less gradient is degenerate but type-legal (`Gradient.stops` has no\n  // non-empty constraint), so a controlled consumer can hand us one. There is\n  // no anchor to place relative to — drop the first stop at 0 and let\n  // sampleStopsAt supply its neutral fallback color.\n  if (sorted.length === 0) return { position: 0, color: sampleStopsAt(sorted, 0) };\n  const selectedIdx = sorted.findIndex((x) => x.id === selectedStopId);\n  const anchorIdx = selectedIdx === -1 ? sorted.length - 1 : selectedIdx;\n  const anchor = sorted[anchorIdx];\n  const next = sorted[anchorIdx + 1];\n  const prev = sorted[anchorIdx - 1];\n  const position = next\n    ? (anchor.position + next.position) / 2\n    : prev\n      ? (prev.position + anchor.position) / 2\n      : Math.min(1, (anchor.position + 1) / 2);\n  return { position, color: sampleStopsAt(sorted, position) };\n}\n"
    },
    {
      "path": "registry/new-york/ui/fill-picker/parts/gradient/stop-color.tsx",
      "type": "registry:ui",
      "target": "@ui/fill-picker/parts/gradient/stop-color.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { ColorPickerContext } from \"../../context\";\nimport { useGradientPickerContext } from \"../../contexts/gradient\";\nimport { useColorPicker } from \"../../hooks/use-color-picker\";\nimport type { OklchColor } from \"../../lib/types\";\n\n/**\n * Mounts ColorPickerContext bound to the selected stop's color. Children are\n * normal ColorPicker.* parts (Area, Hue, ChannelInput, ...) — they Just Work\n * because they only depend on ColorPickerContext.\n *\n * The inner Bound component is keyed on the selected stop id, so a fresh\n * useColorPicker instance (and a fresh `lastGoodHueRef`) is created per\n * stop. This isolates hue preservation per stop: editing stop A then\n * switching to stop B and back to A doesn't smear hue history across stops.\n */\nexport const StopColor: React.FC<React.PropsWithChildren> = ({ children }) => {\n  const grad = useGradientPickerContext();\n  const stop = grad.selectedStop;\n  if (!stop) return null;\n  return (\n    <Bound key={stop.id} stopId={stop.id} initialColor={stop.color}>\n      {children}\n    </Bound>\n  );\n};\n\nconst Bound: React.FC<\n  React.PropsWithChildren<{ stopId: string; initialColor: OklchColor }>\n> = ({ stopId, initialColor, children }) => {\n  const grad = useGradientPickerContext();\n  // Re-read the live color on every render so external moves (e.g. preset\n  // applied) flow into the picker. Memoize on the channel values (not on\n  // grad.stops identity) so the controlled value handed to useColorPicker is\n  // ref-stable across renders that don't actually change this stop's color.\n  // Without this, every gradient update — even ones that don't touch this\n  // stop — would hand the inner ColorPicker a fresh OklchColor object, which\n  // turns Area's `[color]` effect into a per-render trigger and can spiral\n  // into Maximum-update-depth under React 19's stricter detection.\n  const found = grad.stops.find((s) => s.id === stopId);\n  const l = found?.color.l ?? initialColor.l;\n  const c = found?.color.c ?? initialColor.c;\n  const h = found?.color.h ?? initialColor.h;\n  const alpha = found?.color.alpha ?? initialColor.alpha;\n  const liveColor = React.useMemo<OklchColor>(\n    () => ({ l, c, h, alpha }),\n    [l, c, h, alpha],\n  );\n\n  const setStopColor = grad.setStopColor;\n  const onValueChange = React.useCallback(\n    (color: OklchColor) => setStopColor(stopId, color),\n    [setStopColor, stopId],\n  );\n\n  const format = grad.getStopColorFormat(stopId);\n  const onFormatChange = React.useCallback(\n    (f: typeof format) => grad.setStopColorFormat(stopId, f),\n    [grad, stopId],\n  );\n  const state = useColorPicker({\n    value: liveColor,\n    onValueChange,\n    format,\n    onFormatChange,\n  });\n\n  return (\n    <ColorPickerContext.Provider value={state}>\n      {children}\n    </ColorPickerContext.Provider>\n  );\n};\n"
    },
    {
      "path": "registry/new-york/ui/fill-picker/parts/gradient/stop-popover.tsx",
      "type": "registry:ui",
      "target": "@ui/fill-picker/parts/gradient/stop-popover.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Popover } from \"@base-ui/react/popover\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface StopPopoverProps {\n  /** Controlled open — the caller owns it; this is never a self-triggering popover. */\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  /**\n   * The element to position against, rendered in place. It is NOT a trigger:\n   * these anchors (Bar drag handles, StopList swatches) own their own pointer\n   * handling, so open is driven externally and we anchor via Base UI's\n   * Positioner `anchor` prop rather than a Radix-style `PopoverAnchor`.\n   */\n  anchor: React.ReactElement;\n  side?: \"top\" | \"bottom\" | \"left\" | \"right\";\n  align?: \"start\" | \"center\" | \"end\";\n  sideOffset?: number;\n  className?: string;\n  children: React.ReactNode;\n  onContentClick?: React.MouseEventHandler<HTMLDivElement>;\n}\n\n/**\n * Small self-contained popover built directly on `@base-ui/react/popover`.\n *\n * Deliberately does NOT depend on the consumer's `@/components/ui/popover`:\n * shadcn's Base UI popover omits an anchor/asChild surface, so the\n * anchor-without-trigger pattern this editor needs isn't expressible through\n * it. Owning this thin shell keeps the stop editor identical across the Radix\n * and Base UI variants.\n */\nexport function StopPopover({\n  open,\n  onOpenChange,\n  anchor,\n  side = \"bottom\",\n  align = \"center\",\n  sideOffset = 8,\n  className,\n  children,\n  onContentClick,\n}: StopPopoverProps) {\n  const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null);\n  const child = React.Children.only(anchor);\n\n  return (\n    <>\n      {/* Render the anchor in place; capture its DOM node to position against.\n          These anchors carry no ref of their own, so a plain override is safe. */}\n      {React.cloneElement(child as React.ReactElement<{ ref?: React.Ref<HTMLElement> }>, {\n        ref: setAnchorEl,\n      })}\n      <Popover.Root open={open} onOpenChange={(next) => onOpenChange(next)}>\n        <Popover.Portal>\n          <Popover.Positioner\n            anchor={anchorEl}\n            side={side}\n            align={align}\n            sideOffset={sideOffset}\n            className=\"z-50\"\n          >\n            <Popover.Popup\n              onClick={onContentClick}\n              className={cn(\n                \"origin-[var(--transform-origin)] rounded-md border border-border bg-popover p-3 text-popover-foreground shadow-md outline-none\",\n                className,\n              )}\n            >\n              {children}\n            </Popover.Popup>\n          </Popover.Positioner>\n        </Popover.Portal>\n      </Popover.Root>\n    </>\n  );\n}\n"
    },
    {
      "path": "registry/new-york/ui/fill-picker/parts/gradient/stop-editor-shared.ts",
      "type": "registry:ui",
      "target": "@ui/fill-picker/parts/gradient/stop-editor-shared.ts",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { useGradientPickerContext } from \"../../contexts/gradient\";\nimport {\n  useColorPicker,\n  type ColorPickerState,\n} from \"../../hooks/use-color-picker\";\n\n/**\n * Tooltip/popover-agnostic half of the per-stop color editor: a\n * `useColorPicker` bound to one gradient stop's color and per-stop format.\n *\n * Shared by both variants' stop editors (`stop-editor-popover.tsx` here and\n * `fill-picker-base/parts/gradient/stop-editor.tsx`) so the memoization\n * subtleties below can't drift between them — mirrors the\n * `*-shared` split used by GamutBadge / ContrastReadout.\n */\nexport function useStopColorPickerState(stopId: string): ColorPickerState {\n  const grad = useGradientPickerContext();\n  const stop = grad.stops.find((s) => s.id === stopId) ?? grad.stops[0];\n  const format = grad.getStopColorFormat(stopId);\n  // Memoize on the channel values, not on `stop` identity — same reasoning as\n  // `<GradientPicker.StopColor>`'s Bound. The gradient hook allocates fresh\n  // stop objects on every controlled sync (even a structural match), so\n  // passing `stop?.color` straight through would hand the inner picker a new\n  // OklchColor identity on each parent render, turning Area's `[color]`\n  // effect into a per-render trigger.\n  const stopL = stop?.color.l;\n  const stopC = stop?.color.c;\n  const stopH = stop?.color.h;\n  const stopAlpha = stop?.color.alpha;\n  const liveColor = React.useMemo(\n    () =>\n      stopL === undefined ||\n      stopC === undefined ||\n      stopH === undefined ||\n      stopAlpha === undefined\n        ? undefined\n        : { l: stopL, c: stopC, h: stopH, alpha: stopAlpha },\n    [stopL, stopC, stopH, stopAlpha],\n  );\n  return useColorPicker({\n    value: liveColor,\n    onValueChange: (c) => {\n      if (stop) grad.setStopColor(stop.id, c);\n    },\n    format,\n    onFormatChange: (f) => grad.setStopColorFormat(stopId, f),\n  });\n}\n"
    },
    {
      "path": "registry/new-york/ui/fill-picker/parts/gradient/reverse-stops.tsx",
      "type": "registry:ui",
      "target": "@ui/fill-picker/parts/gradient/reverse-stops.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { ArrowLeftRight } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport { useGradientPickerContext } from \"../../contexts/gradient\";\n\n/**\n * Icon button that mirrors every stop's position around 0.5, flipping the\n * gradient's visual order while preserving stop ids and colors. Disabled\n * when there are fewer than 2 stops (nothing meaningful to reverse).\n *\n * Styled with the shared shadcn `<Button variant=\"outline\" size=\"icon-sm\">`\n * so it matches `<ColorPicker.EyeDropper>` and the rest of the new-york\n * icon-button family.\n *\n * Reads from `<GradientPicker.Root>` context — throws if rendered outside.\n */\nexport const ReverseStops = React.forwardRef<\n  HTMLButtonElement,\n  React.ButtonHTMLAttributes<HTMLButtonElement>\n>(function ReverseStops({ className, ...rest }, ref) {\n  const ctx = useGradientPickerContext();\n  const disabled = ctx.stops.length < 2;\n  return (\n    <Button\n      ref={ref}\n      type=\"button\"\n      variant=\"outline\"\n      size=\"icon-sm\"\n      data-slot=\"gradient-reverse-stops\"\n      onClick={ctx.reverseStops}\n      disabled={disabled}\n      aria-label=\"Reverse stop order\"\n      className={cn(\"cursor-pointer\", className)}\n      {...rest}\n    >\n      <ArrowLeftRight aria-hidden=\"true\" className=\"size-4\" />\n    </Button>\n  );\n});\n"
    },
    {
      "path": "registry/new-york/ui/fill-picker/parts/gradient/repeating-toggle.tsx",
      "type": "registry:ui",
      "target": "@ui/fill-picker/parts/gradient/repeating-toggle.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Repeat } from \"lucide-react\";\nimport { Toggle } from \"@/components/ui/toggle\";\nimport { cn } from \"@/lib/utils\";\nimport { useGradientPickerContext } from \"../../contexts/gradient\";\n\n/**\n * Icon toggle that flips the active gradient between its standard and\n * `repeating-*` CSS form. The stop ramp tiles instead of stretching to\n * fill the box — useful for stripes, barberpoles.\n *\n * Built on shadcn `<Toggle variant=\"outline\" size=\"sm\">` so the pressed\n * state is wired by Base UI (`data-pressed`) and the visual treatment\n * matches every other toggle the consumer might already have. We tighten\n * the lit-border affordance over the base variant.\n *\n * Hidden when `gradient.type === \"conic\"` — conic already sweeps a full\n * 360°, so `repeating-conic-gradient` only matters for niche pie-slice\n * patterns; not worth a top-level toggle.\n *\n * Reads from `<GradientPicker.Root>` context — throws if rendered outside.\n */\nexport const RepeatingToggle = React.forwardRef<\n  HTMLButtonElement,\n  React.ComponentProps<typeof Toggle>\n>(function RepeatingToggle({ className, ...rest }, ref) {\n  const ctx = useGradientPickerContext();\n  if (ctx.gradient.type === \"conic\") return null;\n  const on = !!ctx.gradient.repeating;\n  return (\n    <Toggle\n      ref={ref}\n      variant=\"outline\"\n      size=\"sm\"\n      data-slot=\"gradient-repeating-toggle\"\n      pressed={on}\n      onPressedChange={ctx.setRepeating}\n      aria-label={on ? \"Disable repeating gradient\" : \"Enable repeating gradient\"}\n      className={cn(\n        \"cursor-pointer text-muted-foreground data-[pressed]:border-foreground/70 data-[pressed]:bg-foreground/10 data-[pressed]:text-foreground\",\n        className,\n      )}\n      {...rest}\n    >\n      <Repeat aria-hidden=\"true\" className=\"size-4\" />\n    </Toggle>\n  );\n});\n"
    },
    {
      "path": "registry/new-york/ui/fill-picker/parts/gradient/presets.tsx",
      "type": "registry:ui",
      "target": "@ui/fill-picker/parts/gradient/presets.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Plus } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { useGradientPickerContext } from \"../../contexts/gradient\";\nimport {\n  formatGradient,\n  parseGradient,\n  type Gradient,\n} from \"../../lib/gradient\";\nimport { SAMPLE_EDGE } from \"../../lib/constants\";\n\nexport const BUILTIN_GRADIENT_PRESETS: string[] = [\n  \"linear-gradient(in oklch 90deg, oklch(0.9 0.15 60) 0%, oklch(0.6 0.2 30) 100%)\",\n  \"linear-gradient(in oklch 135deg, oklch(0.8 0.18 200) 0%, oklch(0.7 0.2 320) 100%)\",\n  \"linear-gradient(in oklch 0deg, oklch(0.95 0.04 90) 0%, oklch(0.5 0.18 30) 100%)\",\n  \"radial-gradient(circle farthest-corner at 50% 50% in oklch, oklch(0.9 0.2 280) 0%, oklch(0.3 0.12 260) 100%)\",\n  \"conic-gradient(from 0deg at 50% 50% in oklch, oklch(0.7 0.18 0) 0%, oklch(0.7 0.18 90) 25%, oklch(0.7 0.18 180) 50%, oklch(0.7 0.18 270) 75%, oklch(0.7 0.18 360) 100%)\",\n];\n\nexport interface PresetsProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** Override or extend the built-in preset list. */\n  presets?: string[];\n  /**\n   * When provided, renders a \"+\" tile after the presets that calls this with\n   * the current gradient and its serialized CSS. The consumer owns state\n   * (lift `presets` and update it here, persist to localStorage / a server /\n   * a store / whatever).\n   */\n  onAdd?: (gradient: Gradient, css: string) => void;\n}\n\nexport const Presets = React.forwardRef<HTMLDivElement, PresetsProps>(\n  function Presets(\n    { presets = BUILTIN_GRADIENT_PRESETS, onAdd, className, ...rest },\n    ref,\n  ) {\n    const ctx = useGradientPickerContext();\n    const parsed = React.useMemo(\n      () =>\n        presets\n          .map((css) => ({ css, gradient: parseGradient(css) }))\n          .filter((p): p is { css: string; gradient: Gradient } => p.gradient !== null),\n      [presets],\n    );\n\n    return (\n      <div\n        ref={ref}\n        data-slot=\"gradient-presets\"\n        className={cn(\"flex flex-wrap gap-1.5\", className)}\n        {...rest}\n      >\n        {parsed.map(({ css, gradient }) => (\n          <button\n            key={css}\n            type=\"button\"\n            onClick={() => ctx.setGradient(gradient)}\n            className={cn(\n              \"relative size-8 cursor-pointer rounded-md outline-none transition-shadow\",\n              SAMPLE_EDGE,\n              \"focus-visible:ring-2 focus-visible:ring-ring hover:scale-110\",\n            )}\n            style={{ background: formatGradient(gradient) }}\n            aria-label=\"Apply gradient preset\"\n          />\n        ))}\n        {onAdd && (\n          <button\n            type=\"button\"\n            aria-label=\"Save current gradient as preset\"\n            onClick={() => onAdd(ctx.gradient, formatGradient(ctx.gradient))}\n            className={cn(\n              \"inline-flex size-8 cursor-pointer items-center justify-center rounded-md border border-dashed border-border text-muted-foreground outline-none transition-colors\",\n              \"hover:border-foreground hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\",\n            )}\n          >\n            <Plus className=\"size-4\" />\n          </button>\n        )}\n      </div>\n    );\n  },\n);\n"
    },
    {
      "path": "registry/new-york/ui/fill-picker/parts/gradient/css-input.tsx",
      "type": "registry:ui",
      "target": "@ui/fill-picker/parts/gradient/css-input.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { useGradientPickerContext } from \"../../contexts/gradient\";\nimport { formatGradient, parseGradient } from \"../../lib/gradient\";\nimport { FieldInput, FieldShell } from \"../field\";\n\nexport const CssInput = React.forwardRef<\n  HTMLInputElement,\n  React.InputHTMLAttributes<HTMLInputElement>\n>(function CssInput({ className, ...rest }, ref) {\n  const ctx = useGradientPickerContext();\n  const inputRef = React.useRef<HTMLInputElement | null>(null);\n  React.useImperativeHandle(ref, () => inputRef.current as HTMLInputElement);\n  const [draft, setDraft] = React.useState(() => formatGradient(ctx.gradient));\n  const [invalid, setInvalid] = React.useState(false);\n  // \"The user has typed since the last sync/commit.\" Distinct from \"the draft\n  // differs from the current gradient\": an external update while the field is\n  // merely *focused* also makes those differ, and treating that as an edit is\n  // what let a stale draft silently overwrite the newer value on blur.\n  const [dirty, setDirty] = React.useState(false);\n\n  // Adjust state during render rather than via useEffect — same pattern as the\n  // color `CssInput`, `HexField`, and `ChannelField`. Re-sync the draft unless\n  // there is an actual in-progress edit to clobber.\n  const [prevGradient, setPrevGradient] = React.useState(ctx.gradient);\n  if (ctx.gradient !== prevGradient) {\n    setPrevGradient(ctx.gradient);\n    const inputEl = inputRef.current;\n    const focused =\n      typeof document !== \"undefined\" &&\n      inputEl !== null &&\n      document.activeElement === inputEl;\n    if (!focused || !dirty) {\n      setDraft(formatGradient(ctx.gradient));\n      setInvalid(false);\n      setDirty(false);\n    }\n  }\n\n  const commit = () => {\n    // Nothing typed — the draft is just a mirror of the current gradient, so\n    // committing it would at best be a no-op and at worst re-assert a value\n    // that an external update has since replaced.\n    if (!dirty) {\n      setInvalid(false);\n      return;\n    }\n    const parsed = parseGradient(draft);\n    if (parsed) {\n      ctx.setGradient(parsed);\n      setInvalid(false);\n      setDirty(false);\n    } else {\n      setInvalid(true);\n    }\n  };\n\n  return (\n    <FieldShell\n      data-slot=\"gradient-css-input\"\n      className={cn(\n        \"w-full\",\n        invalid && \"border-destructive focus-within:ring-destructive\",\n        className,\n      )}\n    >\n      <FieldInput\n        ref={inputRef}\n        value={draft}\n        onChange={(e) => {\n          setDraft(e.target.value);\n          setDirty(true);\n        }}\n        onBlur={commit}\n        onKeyDown={(e) => {\n          if (e.key === \"Enter\") commit();\n        }}\n        aria-invalid={invalid}\n        aria-label=\"Gradient CSS\"\n        className=\"flex-1 px-2 text-left text-[10px]\"\n        {...rest}\n      />\n    </FieldShell>\n  );\n});\n"
    },
    {
      "path": "registry/new-york/ui/fill-picker/parts/gradient/position-pad.tsx",
      "type": "registry:ui",
      "target": "@ui/fill-picker/parts/gradient/position-pad.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { useGradientPickerContext } from \"../../contexts/gradient\";\nimport { trackPointerDrag } from \"./pointer-drag\";\nimport { useLiveAnnounce } from \"../use-live-announce\";\n\nexport interface PositionPadProps extends React.HTMLAttributes<HTMLDivElement> {\n  size?: number;\n}\n\nexport const PositionPad = React.forwardRef<HTMLDivElement, PositionPadProps>(\n  function PositionPad({ className, size = 32, ...rest }, ref) {\n    const ctx = useGradientPickerContext();\n    const padRef = React.useRef<HTMLDivElement | null>(null);\n    React.useImperativeHandle(ref, () => padRef.current as HTMLDivElement);\n    // role=\"application\" is silent under keyboard — announce moves politely.\n    const [liveText, announce] = useLiveAnnounce();\n\n    if (ctx.gradient.type === \"linear\") return null;\n    const center = ctx.gradient.center;\n\n    const fromEvent = (clientX: number, clientY: number) => {\n      const el = padRef.current;\n      if (!el) return center;\n      const r = el.getBoundingClientRect();\n      if (r.width === 0 || r.height === 0) return { x: 0, y: 0 };\n      return {\n        x: Math.max(0, Math.min(1, (clientX - r.left) / r.width)),\n        y: Math.max(0, Math.min(1, (clientY - r.top) / r.height)),\n      };\n    };\n\n    const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {\n      ctx.setCenter(fromEvent(e.clientX, e.clientY));\n      trackPointerDrag(e.currentTarget, e.pointerId, (ev) =>\n        ctx.setCenter(fromEvent(ev.clientX, ev.clientY)),\n      );\n    };\n\n    const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {\n      const step = e.shiftKey ? 0.05 : 0.01;\n      const clamp = (n: number) => Math.max(0, Math.min(1, n));\n      let next: { x: number; y: number };\n      if (e.key === \"ArrowLeft\") {\n        next = { x: clamp(center.x - step), y: center.y };\n      } else if (e.key === \"ArrowRight\") {\n        next = { x: clamp(center.x + step), y: center.y };\n      } else if (e.key === \"ArrowUp\") {\n        next = { x: center.x, y: clamp(center.y - step) };\n      } else if (e.key === \"ArrowDown\") {\n        next = { x: center.x, y: clamp(center.y + step) };\n      } else {\n        return;\n      }\n      e.preventDefault();\n      ctx.setCenter(next);\n      announce(\n        `x ${Math.round(next.x * 100)}%, y ${Math.round(next.y * 100)}%`,\n      );\n    };\n\n    return (\n      <div\n        ref={padRef}\n        data-slot=\"gradient-position-pad\"\n        onPointerDown={onPointerDown}\n        onKeyDown={onKeyDown}\n        role=\"application\"\n        aria-label={`Gradient position, x ${Math.round(center.x * 100)}%, y ${Math.round(center.y * 100)}%`}\n        aria-roledescription=\"2D pad for gradient center\"\n        tabIndex={0}\n        style={{ width: size, height: size }}\n        className={cn(\n          \"relative shrink-0 cursor-crosshair touch-none rounded-md border border-border bg-muted outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n          className,\n        )}\n        {...rest}\n      >\n        <span aria-live=\"polite\" className=\"sr-only\">\n          {liveText}\n        </span>\n        <div\n          aria-hidden\n          className=\"absolute size-2 -translate-x-1/2 -translate-y-1/2 rounded-full border border-background bg-foreground\"\n          style={{ left: `${center.x * 100}%`, top: `${center.y * 100}%` }}\n        />\n      </div>\n    );\n  },\n);\n"
    },
    {
      "path": "registry/new-york/ui/fill-picker/parts/gradient/position-input.tsx",
      "type": "registry:ui",
      "target": "@ui/fill-picker/parts/gradient/position-input.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { useGradientPickerContext } from \"../../contexts/gradient\";\nimport {\n  FieldDivider,\n  FieldDraftInput,\n  FieldInputGroup,\n  FieldShell,\n  FieldSuffix,\n} from \"../field\";\n\nexport const PositionInput = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(function PositionInput({ className, ...rest }, ref) {\n  const ctx = useGradientPickerContext();\n  if (ctx.gradient.type === \"linear\") return null;\n  const center = ctx.gradient.center;\n\n  const commit = (axis: \"x\" | \"y\", raw: string) => {\n    if (raw === \"\") return;\n    const n = parseFloat(raw);\n    if (!Number.isFinite(n)) return;\n    const clamp = (v: number) => Math.max(0, Math.min(1, v / 100));\n    ctx.setCenter(\n      axis === \"x\"\n        ? { x: clamp(n), y: center.y }\n        : { x: center.x, y: clamp(n) },\n    );\n  };\n\n  return (\n    <FieldShell\n      ref={ref}\n      data-slot=\"gradient-position-input\"\n      className={cn(\"min-w-0 flex-1\", className)}\n      {...rest}\n    >\n      <FieldInputGroup>\n        <span className=\"sr-only\">Gradient center x</span>\n        <FieldDraftInput\n          inputMode=\"decimal\"\n          nudge={1}\n          value={String(Math.round(center.x * 100))}\n          onCommit={(raw) => commit(\"x\", raw)}\n          aria-label=\"Gradient center x percent\"\n          className=\"w-10\"\n        />\n        <FieldSuffix>%</FieldSuffix>\n      </FieldInputGroup>\n      <FieldDivider />\n      <FieldInputGroup>\n        <span className=\"sr-only\">Gradient center y</span>\n        <FieldDraftInput\n          inputMode=\"decimal\"\n          nudge={1}\n          value={String(Math.round(center.y * 100))}\n          onCommit={(raw) => commit(\"y\", raw)}\n          aria-label=\"Gradient center y percent\"\n          className=\"w-10\"\n        />\n        <FieldSuffix>%</FieldSuffix>\n      </FieldInputGroup>\n    </FieldShell>\n  );\n});\n"
    },
    {
      "path": "registry/new-york/ui/fill-picker/parts/gradient/position-group.tsx",
      "type": "registry:ui",
      "target": "@ui/fill-picker/parts/gradient/position-group.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { useGradientPickerContext } from \"../../contexts/gradient\";\n\nexport const PositionGroup = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(function PositionGroup({ className, children, ...rest }, ref) {\n  const ctx = useGradientPickerContext();\n  if (ctx.gradient.type === \"linear\") return null;\n  return (\n    <div\n      ref={ref}\n      data-slot=\"gradient-position-group\"\n      className={cn(\"flex w-full items-center gap-2\", className)}\n      {...rest}\n    >\n      {children}\n    </div>\n  );\n});\n"
    },
    {
      "path": "registry/new-york/ui/fill-picker/parts/gradient/angle-pad.tsx",
      "type": "registry:ui",
      "target": "@ui/fill-picker/parts/gradient/angle-pad.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { useGradientPickerContext } from \"../../contexts/gradient\";\nimport { trackPointerDrag } from \"./pointer-drag\";\n\nexport interface AnglePadProps extends React.HTMLAttributes<HTMLDivElement> {\n  size?: number;\n}\n\nexport const AnglePad = React.forwardRef<HTMLDivElement, AnglePadProps>(\n  function AnglePad({ className, size = 32, ...rest }, ref) {\n    const ctx = useGradientPickerContext();\n    const padRef = React.useRef<HTMLDivElement | null>(null);\n    React.useImperativeHandle(ref, () => padRef.current as HTMLDivElement);\n\n    if (ctx.gradient.type === \"radial\") return null;\n\n    const angle =\n      ctx.gradient.type === \"linear\"\n        ? ctx.gradient.angle\n        : ctx.gradient.startAngle;\n    const setAngle =\n      ctx.gradient.type === \"linear\" ? ctx.setAngle : ctx.setStartAngle;\n\n    const fromEvent = (clientX: number, clientY: number): number => {\n      const el = padRef.current;\n      if (!el) return angle;\n      const r = el.getBoundingClientRect();\n      const cx = r.left + r.width / 2;\n      const cy = r.top + r.height / 2;\n      const dx = clientX - cx;\n      const dy = clientY - cy;\n      const deg = (Math.atan2(dx, -dy) * 180) / Math.PI;\n      return (deg + 360) % 360;\n    };\n\n    const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {\n      setAngle(fromEvent(e.clientX, e.clientY));\n      trackPointerDrag(e.currentTarget, e.pointerId, (ev) =>\n        setAngle(fromEvent(ev.clientX, ev.clientY)),\n      );\n    };\n\n    const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {\n      const step = e.shiftKey ? 15 : 1;\n      let next = angle;\n      if (e.key === \"ArrowLeft\" || e.key === \"ArrowDown\") next = angle - step;\n      else if (e.key === \"ArrowRight\" || e.key === \"ArrowUp\") next = angle + step;\n      else if (e.key === \"Home\") next = 0;\n      else if (e.key === \"End\") next = 360;\n      else return;\n      e.preventDefault();\n      setAngle(((next % 360) + 360) % 360);\n    };\n\n    return (\n      <div\n        ref={padRef}\n        onPointerDown={onPointerDown}\n        onKeyDown={onKeyDown}\n        role=\"slider\"\n        aria-label=\"Gradient angle\"\n        aria-valuemin={0}\n        aria-valuemax={360}\n        aria-valuenow={Math.round(angle)}\n        aria-valuetext={`${Math.round(angle)} degrees`}\n        tabIndex={0}\n        data-slot=\"gradient-angle-pad\"\n        className={cn(\n          \"relative shrink-0 cursor-grab touch-none rounded-full border border-border bg-muted outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n          className,\n        )}\n        style={{ width: size, height: size }}\n        {...rest}\n      >\n        <div\n          aria-hidden\n          className=\"absolute left-1/2 top-1/2 size-0 origin-center\"\n          style={{ transform: `translate(-50%, -50%) rotate(${angle}deg)` }}\n        >\n          <div\n            className=\"absolute left-1/2 size-1.5 -translate-x-1/2 rounded-full bg-foreground shadow-sm ring-1 ring-background/40\"\n            style={{ top: `calc(-${size / 2}px + 4px)` }}\n          />\n        </div>\n      </div>\n    );\n  },\n);\n"
    },
    {
      "path": "registry/new-york/ui/fill-picker/parts/gradient/angle-input.tsx",
      "type": "registry:ui",
      "target": "@ui/fill-picker/parts/gradient/angle-input.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { useGradientPickerContext } from \"../../contexts/gradient\";\nimport {\n  FieldDraftInput,\n  FieldInputGroup,\n  FieldShell,\n  FieldSuffix,\n} from \"../field\";\n\nexport const AngleInput = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(function AngleInput({ className, ...rest }, ref) {\n  const ctx = useGradientPickerContext();\n  if (ctx.gradient.type === \"radial\") return null;\n\n  const angle =\n    ctx.gradient.type === \"linear\"\n      ? ctx.gradient.angle\n      : ctx.gradient.startAngle;\n  const setAngle =\n    ctx.gradient.type === \"linear\" ? ctx.setAngle : ctx.setStartAngle;\n\n  const commit = (raw: string) => {\n    const n = parseFloat(raw);\n    if (Number.isFinite(n)) setAngle(((n % 360) + 360) % 360);\n  };\n\n  return (\n    <FieldShell\n      ref={ref}\n      data-slot=\"gradient-angle-input\"\n      className={cn(\"min-w-0 flex-1\", className)}\n      {...rest}\n    >\n      <FieldInputGroup>\n        <span className=\"sr-only\">Gradient angle</span>\n        <FieldDraftInput\n          inputMode=\"decimal\"\n          nudge={1}\n          value={String(Math.round(angle))}\n          onCommit={commit}\n          aria-label=\"Gradient angle in degrees\"\n          className=\"w-10\"\n        />\n        <FieldSuffix>°</FieldSuffix>\n      </FieldInputGroup>\n    </FieldShell>\n  );\n});\n"
    },
    {
      "path": "registry/new-york/ui/fill-picker/parts/gradient/angle-group.tsx",
      "type": "registry:ui",
      "target": "@ui/fill-picker/parts/gradient/angle-group.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { useGradientPickerContext } from \"../../contexts/gradient\";\n\nexport const AngleGroup = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(function AngleGroup({ className, children, ...rest }, ref) {\n  const ctx = useGradientPickerContext();\n  if (ctx.gradient.type === \"radial\") return null;\n  return (\n    <div\n      ref={ref}\n      data-slot=\"gradient-angle-group\"\n      className={cn(\"flex w-full items-center gap-2\", className)}\n      {...rest}\n    >\n      {children}\n    </div>\n  );\n});\n"
    },
    {
      "path": "registry/new-york/ui/fill-picker/parts/gradient/shape-switcher.tsx",
      "type": "registry:ui",
      "target": "@ui/fill-picker/parts/gradient/shape-switcher.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { useGradientPickerContext } from \"../../contexts/gradient\";\n\nconst OPTIONS = [\n  { value: \"circle\", label: \"Circle\" },\n  { value: \"ellipse\", label: \"Ellipse\" },\n] as const;\n\nexport const ShapeSwitcher = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(function ShapeSwitcher({ className, ...rest }, ref) {\n  const ctx = useGradientPickerContext();\n  if (ctx.gradient.type !== \"radial\") return null;\n  const current = ctx.gradient.shape;\n\n  // role=\"group\" + aria-pressed buttons rather than role=\"tablist\" / \"tab\":\n  // the WAI-ARIA tab pattern requires arrow-key navigation and tabpanels,\n  // neither of which apply here. This is a segmented toggle, and shadcn's\n  // own Toggle uses aria-pressed for the same reason. Each button is\n  // independently Tab-reachable; activation is via Space/Enter (native\n  // <button> behavior).\n  return (\n    <div\n      ref={ref}\n      role=\"group\"\n      aria-label=\"Radial gradient shape\"\n      data-slot=\"gradient-shape-switcher\"\n      className={cn(\n        \"inline-flex w-full rounded-md bg-muted p-0.75 gap-0.5\",\n        className,\n      )}\n      {...rest}\n    >\n      {OPTIONS.map(({ value, label }) => {\n        const active = current === value;\n        return (\n          <button\n            key={value}\n            type=\"button\"\n            aria-pressed={active}\n            onClick={() => ctx.setRadialShape(value)}\n            className={cn(\n              \"flex-1 rounded-sm px-3 py-1 text-xs font-medium outline-none transition-colors\",\n              \"focus-visible:ring-2 focus-visible:ring-ring\",\n              active\n                ? \"bg-background text-foreground shadow-sm\"\n                : \"text-muted-foreground hover:text-foreground\",\n            )}\n          >\n            {label}\n          </button>\n        );\n      })}\n    </div>\n  );\n});\n"
    },
    {
      "path": "registry/new-york/ui/fill-picker/parts/gradient/radius-input.tsx",
      "type": "registry:ui",
      "target": "@ui/fill-picker/parts/gradient/radius-input.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { useGradientPickerContext } from \"../../contexts/gradient\";\nimport {\n  FieldDraftInput,\n  FieldInputGroup,\n  FieldShell,\n  FieldSuffix,\n} from \"../field\";\n\nexport const RadiusInput = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(function RadiusInput({ className, ...rest }, ref) {\n  const ctx = useGradientPickerContext();\n  if (ctx.gradient.type !== \"radial\") return null;\n  if (ctx.gradient.shape !== \"circle\") return null;\n  const g = ctx.gradient;\n  const usePercent = !!ctx.containerWidth;\n  const display = (() => {\n    if (g.radiusPx === undefined) return \"\";\n    return String(\n      usePercent\n        ? Math.round((g.radiusPx / (ctx.containerWidth as number)) * 100)\n        : Math.round(g.radiusPx),\n    );\n  })();\n\n  const commit = (raw: string) => {\n    const v = raw.trim();\n    if (v === \"\") {\n      ctx.setRadiusPx(undefined);\n      return;\n    }\n    const n = parseFloat(v);\n    if (!Number.isFinite(n)) return;\n    if (!usePercent) {\n      ctx.setRadiusPx(Math.max(0, n));\n      return;\n    }\n    ctx.setRadiusPx(Math.max(0, (n / 100) * (ctx.containerWidth as number)));\n  };\n\n  return (\n    <FieldShell\n      ref={ref}\n      data-slot=\"gradient-radius-input\"\n      className={cn(\"min-w-0 shrink-0\", className)}\n      {...rest}\n    >\n      <FieldInputGroup>\n        <span className=\"sr-only\">Circle radius</span>\n        <FieldDraftInput\n          inputMode=\"decimal\"\n          nudge={1}\n          value={display}\n          placeholder=\"auto\"\n          onCommit={commit}\n          aria-label={\n            usePercent\n              ? \"Circle radius as percent of Area width\"\n              : \"Circle radius in pixels\"\n          }\n          className=\"w-12\"\n        />\n        <FieldSuffix>{usePercent ? \"%\" : \"px\"}</FieldSuffix>\n      </FieldInputGroup>\n    </FieldShell>\n  );\n});\n"
    },
    {
      "path": "registry/new-york/ui/fill-picker/parts/gradient/ellipse-radii-input.tsx",
      "type": "registry:ui",
      "target": "@ui/fill-picker/parts/gradient/ellipse-radii-input.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { useGradientPickerContext } from \"../../contexts/gradient\";\nimport {\n  FieldDivider,\n  FieldDraftInput,\n  FieldInputGroup,\n  FieldShell,\n  FieldSuffix,\n} from \"../field\";\nimport { keywordToRadii } from \"./overlay\";\n\nexport const EllipseRadiiInput = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(function EllipseRadiiInput({ className, ...rest }, ref) {\n  const ctx = useGradientPickerContext();\n  if (ctx.gradient.type !== \"radial\") return null;\n  if (ctx.gradient.shape !== \"ellipse\") return null;\n  const g = ctx.gradient;\n\n  const commit = (axis: \"x\" | \"y\", raw: string) => {\n    if (raw.trim() === \"\") {\n      ctx.setRadii(undefined);\n      return;\n    }\n    const n = parseFloat(raw);\n    if (!Number.isFinite(n)) return;\n    // Keep the untouched axis at its *effective* extent. For an ellipse the\n    // keyword seed is already normalized per axis, so the box size cancels\n    // out (1 × 1) — the same seed the Overlay's edge handle starts from.\n    const current =\n      g.radii ?? keywordToRadii(\"ellipse\", g.size, g.center, 1, 1);\n    ctx.setRadii(\n      axis === \"x\"\n        ? { x: Math.max(0, n / 100), y: current.y }\n        : { x: current.x, y: Math.max(0, n / 100) },\n    );\n  };\n\n  return (\n    <FieldShell\n      ref={ref}\n      data-slot=\"gradient-ellipse-radii-input\"\n      className={cn(\"min-w-0 shrink-0\", className)}\n      {...rest}\n    >\n      <FieldInputGroup>\n        <span className=\"sr-only\">Ellipse horizontal radius</span>\n        <FieldDraftInput\n          inputMode=\"decimal\"\n          nudge={1}\n          value={g.radii ? String(Math.round(g.radii.x * 100)) : \"\"}\n          placeholder=\"auto\"\n          onCommit={(raw) => commit(\"x\", raw)}\n          aria-label=\"Ellipse horizontal radius percent\"\n          className=\"w-12\"\n        />\n      </FieldInputGroup>\n      <FieldDivider />\n      <FieldInputGroup>\n        <span className=\"sr-only\">Ellipse vertical radius</span>\n        <FieldDraftInput\n          inputMode=\"decimal\"\n          nudge={1}\n          value={g.radii ? String(Math.round(g.radii.y * 100)) : \"\"}\n          placeholder=\"auto\"\n          onCommit={(raw) => commit(\"y\", raw)}\n          aria-label=\"Ellipse vertical radius percent\"\n          className=\"w-12\"\n        />\n        <FieldSuffix>%</FieldSuffix>\n      </FieldInputGroup>\n    </FieldShell>\n  );\n});\n"
    }
  ]
}
