{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "shape-path-editor",
  "title": "Shape Path Editor",
  "description": "Ridiculously typed editor for the CSS shape() function (clip-path / offset-path). The strict tier dispatches each command on its name (move/line/curve/arc/…), checking arity, direction, and every coordinate. Ships the registry's only Bézier-control-handle canvas with a live preview.",
  "dependencies": [],
  "registryDependencies": [
    "ridiculous-type-kit",
    "unit-input",
    "button",
    "popover",
    "input",
    "label"
  ],
  "files": [
    {
      "path": "src/components/ui/shape-path-editor/index.ts",
      "content": "export type {\n  CommandRowProps,\n  MiniSelectProps,\n  ShapeCanvasProps,\n  ShapePathEditorMode,\n  ShapePathEditorPanelProps,\n  ShapePathEditorProps,\n  ShapePreviewMode,\n  ShapePreviewProps,\n} from \"./shape-path-editor\"\nexport {\n  CommandRow,\n  LiveString,\n  MiniSelect,\n  ShapeCanvas,\n  ShapePathEditor,\n  ShapePathEditorPanel,\n  ShapePreview,\n} from \"./shape-path-editor\"\nexport type { CanvasPoint, PointRole } from \"./shape-path-editor.helpers\"\nexport {\n  commandArity,\n  commandNames,\n  defaultShape,\n  formatShape,\n  isCommandName,\n  parseShape,\n  shapeToPoints,\n  updatePoint,\n} from \"./shape-path-editor.helpers\"\nexport type {\n  CommandCountOf,\n  CommandsOf,\n  Point,\n  ShapeCommand,\n  ShapeCommandName,\n  ShapeLiteral,\n  ShapeString,\n  ShapeValue,\n} from \"./shape-path-editor.types\"\nexport { cssShape } from \"./shape-path-editor.types\"\n",
      "type": "registry:ui",
      "target": "components/ui/shape-path-editor/index.ts"
    },
    {
      "path": "src/components/ui/shape-path-editor/shape-path-editor.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useRef, useState } from \"react\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\"\nimport { cn } from \"@/lib/utils\"\nimport { CommandRow } from \"./command-row\"\nimport { ShapeCanvas } from \"./shape-canvas\"\nimport {\n  defaultShape,\n  formatShape,\n  parseShape,\n} from \"./shape-path-editor.helpers\"\nimport type {\n  ShapeCommand,\n  ShapeString,\n  ShapeValue,\n} from \"./shape-path-editor.types\"\nimport { ShapePreview } from \"./shape-preview\"\n\n// Re-export the public sub-components + their prop types so consumers (and the\n// barrel) can import them from `./shape-path-editor`.\nexport type { CommandRowProps } from \"./command-row\"\nexport { CommandRow } from \"./command-row\"\nexport type { MiniSelectProps } from \"./mini-select\"\nexport { MiniSelect } from \"./mini-select\"\nexport type { ShapeCanvasProps } from \"./shape-canvas\"\nexport { ShapeCanvas } from \"./shape-canvas\"\nexport type { ShapePreviewMode, ShapePreviewProps } from \"./shape-preview\"\nexport { ShapePreview } from \"./shape-preview\"\n\n// ---------------------------------------------------------------------------\n// Shared props\n// ---------------------------------------------------------------------------\n\nexport type ShapePathEditorMode = \"clip-path\" | \"offset-path\"\n\nexport interface ShapePathEditorPanelProps {\n  value: ShapeString | (string & {})\n  onChange: (value: ShapeString) => void\n  /**\n   * Which CSS property the live preview targets. Both `clip-path` and\n   * `offset-path` share the identical `shape()` grammar, so this does NOT\n   * change validation or narrow the `onChange` output — it only drives the\n   * preview render. Defaults to `\"clip-path\"`.\n   */\n  mode?: ShapePathEditorMode\n  className?: string\n  \"aria-label\"?: string\n}\n\nexport interface ShapePathEditorProps extends ShapePathEditorPanelProps {}\n\n// ---------------------------------------------------------------------------\n// ShapePathEditor — popover-wrapped\n// ---------------------------------------------------------------------------\n\nexport function ShapePathEditor(props: ShapePathEditorProps) {\n  const {\n    value,\n    className,\n    \"aria-label\": ariaLabel = \"Edit a shape() path\",\n  } = props\n  const parsed = parseShape(String(value))\n  const label =\n    parsed.error !== null\n      ? \"invalid\"\n      : `${parsed.commands.length} cmd${parsed.commands.length === 1 ? \"\" : \"s\"}`\n\n  return (\n    <Popover>\n      <PopoverTrigger asChild>\n        <Button\n          variant=\"outline\"\n          className={cn(\"h-9 gap-2 px-3 font-mono\", className)}\n          aria-label={ariaLabel}\n        >\n          <span aria-hidden=\"true\" className=\"text-foreground/60\">\n            ✎\n          </span>\n          <span className=\"text-[10px] text-muted-foreground\">{label}</span>\n          <span className=\"max-w-[220px] truncate text-xs\">{value}</span>\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent className=\"w-auto p-0\" align=\"start\">\n        <ShapePathEditorPanel {...props} />\n      </PopoverContent>\n    </Popover>\n  )\n}\n\n// ---------------------------------------------------------------------------\n// ShapePathEditorPanel — inline\n// ---------------------------------------------------------------------------\n\nfunction toValue(parsed: ReturnType<typeof parseShape>): ShapeValue {\n  return {\n    fillRule: parsed.fillRule,\n    from: parsed.from,\n    commands: parsed.commands,\n  }\n}\n\nexport function ShapePathEditorPanel({\n  value,\n  onChange,\n  mode = \"clip-path\",\n  className,\n  \"aria-label\": ariaLabel = \"CSS shape() path editor\",\n}: ShapePathEditorPanelProps) {\n  const [shape, setShape] = useState<ShapeValue>(() =>\n    toValue(parseShape(String(value) || defaultShape())),\n  )\n  const lastEmittedRef = useRef<string | null>(null)\n\n  // Resync from an external value (skip our own emits).\n  useEffect(() => {\n    if (value === lastEmittedRef.current) return\n    const parsed = parseShape(String(value))\n    if (parsed.error === null) setShape(toValue(parsed))\n  }, [value])\n\n  const commit = (next: ShapeValue) => {\n    setShape(next)\n    const str = formatShape(next)\n    lastEmittedRef.current = str\n    onChange(str as ShapeString)\n  }\n\n  // canvas drag/nudge passes a fully-formatted string; re-parse + commit.\n  const commitString = (str: string) => {\n    const parsed = parseShape(str)\n    if (parsed.error === null) commit(toValue(parsed))\n  }\n\n  const updateCommand = (index: number, cmd: ShapeCommand) => {\n    commit({\n      ...shape,\n      commands: shape.commands.map((c, i) => (i === index ? cmd : c)),\n    })\n  }\n\n  const addCommand = () => {\n    // Insert a fresh `line` before a trailing `close` (or at the end).\n    const fresh: ShapeCommand = {\n      kind: \"line\",\n      by: false,\n      to: { x: \"50px\", y: \"50px\" },\n    }\n    const cmds = shape.commands\n    const lastIsClose =\n      cmds.length > 0 && cmds[cmds.length - 1].kind === \"close\"\n    const at = lastIsClose ? cmds.length - 1 : cmds.length\n    const next = [...cmds.slice(0, at), fresh, ...cmds.slice(at)]\n    commit({ ...shape, commands: next })\n  }\n\n  const removeCommand = (index: number) => {\n    commit({\n      ...shape,\n      commands: shape.commands.filter((_, i) => i !== index),\n    })\n  }\n\n  const moveCommand = (index: number, dir: -1 | 1) => {\n    const target = index + dir\n    if (target < 0 || target >= shape.commands.length) return\n    const next = [...shape.commands]\n    const [moved] = next.splice(index, 1)\n    next.splice(target, 0, moved)\n    commit({ ...shape, commands: next })\n  }\n\n  const produced = formatShape(shape)\n\n  return (\n    <fieldset\n      className={cn(\n        \"m-0 w-[520px] space-y-3 border-0 bg-background p-3\",\n        className,\n      )}\n      aria-label={ariaLabel}\n    >\n      <ShapeCanvas value={produced} onChange={commitString} />\n\n      <div className=\"space-y-1.5\">\n        <div className=\"max-h-[220px] space-y-1.5 overflow-y-auto pr-1\">\n          {shape.commands.map((cmd, i) => (\n            <CommandRow\n              // biome-ignore lint/suspicious/noArrayIndexKey: commands are a positional list edited in place; index is the stable identity.\n              key={i}\n              index={i}\n              command={cmd}\n              onChange={(next) => updateCommand(i, next)}\n              onRemove={() => removeCommand(i)}\n              onMoveUp={() => moveCommand(i, -1)}\n              onMoveDown={() => moveCommand(i, 1)}\n              canMoveUp={i > 0}\n              canMoveDown={i < shape.commands.length - 1}\n            />\n          ))}\n        </div>\n        <button\n          type=\"button\"\n          aria-label=\"Add command\"\n          onClick={addCommand}\n          className=\"h-8 w-full rounded border border-dashed font-mono text-[10px] text-muted-foreground hover:text-foreground\"\n        >\n          + add command\n        </button>\n      </div>\n\n      <LiveString value={produced} />\n\n      <ShapePreview value={produced} mode={mode} />\n    </fieldset>\n  )\n}\n\n// ---------------------------------------------------------------------------\n// LiveString — the produced value in a `<code>` (internal helper, exported\n// for parity with the sibling sub-components and demos).\n// ---------------------------------------------------------------------------\n\nexport function LiveString({ value }: { value: string }) {\n  return (\n    <code className=\"block overflow-x-auto rounded bg-muted/50 px-2 py-1.5 font-mono text-foreground text-xs\">\n      {value || \" \"}\n    </code>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/shape-path-editor/shape-path-editor.tsx"
    },
    {
      "path": "src/components/ui/shape-path-editor/shape-path-editor.types.ts",
      "content": "// =====================================================================\n// shape-path-editor.types.ts — ridiculously typed CSS shape() function\n// (CSS Shapes L2; for clip-path / offset-path).\n//\n// ShapeLiteral<S> peels the shape() wrapper, validates the optional\n// fill-rule + the `from <coordinate-pair>` seed, then dispatches each\n// comma-separated command on its name (move/line/hline/vline/curve/\n// smooth/arc/close) — per-command arity, the by/to direction keyword, the\n// with/of slot keywords, and every coordinate's dimension. In the\n// transform-builder / clip-path-editor function-dispatch lineage.\n//\n// NOTE: coordinates are <length-percentage> — the kit's IsLength requires a\n// unit, so `0px` / `0%` are required (bare `0` is rejected, consistent with\n// every other ridiculous component). hline/vline keyword positions + arc\n// flags are deferred to the runtime parser (see spec §3.1).\n//\n// Spec: docs/superpowers/specs/2026-06-19-shape-path-editor-design.md\n// =====================================================================\n\nimport type {\n  And,\n  IsLength,\n  IsPercentage,\n  KeepIf,\n  Or,\n  ParseFunction,\n  SplitByComma,\n  SplitBySpace,\n} from \"@/lib/ridiculous-type-kit\"\n\n/** A `<length-percentage>` token. */\ntype LP<S extends string> = Or<IsLength<S>, IsPercentage<S>>\n\ntype IsByTo<S extends string> = S extends \"by\" | \"to\" ? true : false\n\n/** The eight `<shape-command>` names. */\nexport type ShapeCommandName =\n  | \"move\"\n  | \"line\"\n  | \"hline\"\n  | \"vline\"\n  | \"curve\"\n  | \"smooth\"\n  | \"arc\"\n  | \"close\"\n\ntype FillRule = \"nonzero\" | \"evenodd\"\n\n// ---------------------------------------------------------------------------\n// the `from` seed segment (optional fill-rule + from <coordinate-pair>)\n// ---------------------------------------------------------------------------\n\ntype ValidFrom<Toks extends string[]> = Toks extends [\n  \"from\",\n  infer X extends string,\n  infer Y extends string,\n]\n  ? And<LP<X>, LP<Y>>\n  : false\n\ntype ValidFromSeg<Seg extends string> =\n  SplitBySpace<Seg> extends [\n    infer A extends string,\n    ...infer Rest extends string[],\n  ]\n    ? A extends FillRule\n      ? ValidFrom<Rest>\n      : ValidFrom<[A, ...Rest]>\n    : false\n\n// ---------------------------------------------------------------------------\n// per-command validators\n// ---------------------------------------------------------------------------\n\n// move|line : <by|to> <x> <y>\ntype ValidLineLike<Rest extends string[]> = Rest extends [\n  infer D extends string,\n  infer X extends string,\n  infer Y extends string,\n]\n  ? And<IsByTo<D>, And<LP<X>, LP<Y>>>\n  : false\n\n// hline|vline : <by|to> <length-percentage>\ntype ValidHV<Rest extends string[]> = Rest extends [\n  infer D extends string,\n  infer V extends string,\n]\n  ? And<IsByTo<D>, LP<V>>\n  : false\n\n// the control-point tail of curve: <cx> <cy> [ / <cx2> <cy2> ]\ntype ValidControlTail<Ctrl extends string[]> = Ctrl extends [\n  infer X1 extends string,\n  infer Y1 extends string,\n]\n  ? And<LP<X1>, LP<Y1>>\n  : Ctrl extends [\n        infer X1 extends string,\n        infer Y1 extends string,\n        \"/\",\n        infer X2 extends string,\n        infer Y2 extends string,\n      ]\n    ? And<And<LP<X1>, LP<Y1>>, And<LP<X2>, LP<Y2>>>\n    : false\n\n// curve : <by|to> <x> <y> with <control-tail>\ntype ValidCurve<Rest extends string[]> = Rest extends [\n  infer D extends string,\n  infer X extends string,\n  infer Y extends string,\n  \"with\",\n  ...infer Ctrl extends string[],\n]\n  ? And<IsByTo<D>, And<And<LP<X>, LP<Y>>, ValidControlTail<Ctrl>>>\n  : false\n\n// smooth : <by|to> <x> <y> [ with <cx> <cy> ]\ntype ValidSmooth<Rest extends string[]> = Rest extends [\n  infer D extends string,\n  infer X extends string,\n  infer Y extends string,\n]\n  ? And<IsByTo<D>, And<LP<X>, LP<Y>>>\n  : Rest extends [\n        infer D extends string,\n        infer X extends string,\n        infer Y extends string,\n        \"with\",\n        infer CX extends string,\n        infer CY extends string,\n      ]\n    ? And<IsByTo<D>, And<And<LP<X>, LP<Y>>, And<LP<CX>, LP<CY>>>>\n    : false\n\n// arc : <by|to> <x> <y> of <r> [ <r2> <flags…> ]  (tail lenient — A5)\ntype ValidArc<Rest extends string[]> = Rest extends [\n  infer D extends string,\n  infer X extends string,\n  infer Y extends string,\n  \"of\",\n  infer R extends string,\n  ...string[],\n]\n  ? And<IsByTo<D>, And<And<LP<X>, LP<Y>>, LP<R>>>\n  : false\n\ntype ValidCommand<C extends string> =\n  SplitBySpace<C> extends [\n    infer Name extends string,\n    ...infer Rest extends string[],\n  ]\n    ? Name extends \"close\"\n      ? Rest extends []\n        ? true\n        : false\n      : Name extends \"move\" | \"line\"\n        ? ValidLineLike<Rest>\n        : Name extends \"hline\" | \"vline\"\n          ? ValidHV<Rest>\n          : Name extends \"curve\"\n            ? ValidCurve<Rest>\n            : Name extends \"smooth\"\n              ? ValidSmooth<Rest>\n              : Name extends \"arc\"\n                ? ValidArc<Rest>\n                : false\n    : false\n\ntype AllCommands<Cmds extends string[]> = Cmds extends [\n  infer H extends string,\n  ...infer R extends string[],\n]\n  ? ValidCommand<H> extends true\n    ? AllCommands<R>\n    : false\n  : true\n\ntype ValidateShapeBody<Args extends string> =\n  SplitByComma<Args> extends [\n    infer First extends string,\n    ...infer Cmds extends string[],\n  ]\n    ? ValidFromSeg<First> extends true\n      ? AllCommands<Cmds>\n      : false\n    : false\n\n/** Strict validator for a CSS `shape()` value. `S` or `never`. */\nexport type ShapeLiteral<S extends string> =\n  ParseFunction<S> extends {\n    name: \"shape\"\n    args: infer Args extends string\n  }\n    ? KeepIf<ValidateShapeBody<Args>, S>\n    : never\n\n// ---------------------------------------------------------------------------\n// call-site helper + suggestion + utility\n// ---------------------------------------------------------------------------\n\nexport const cssShape = <S extends string>(value: S & ShapeLiteral<S>): S =>\n  value\n\nexport type ShapeString = `shape(${string})` | (string & {})\n\n/** The command segments of a shape() value (everything after the `from` seed). */\nexport type CommandsOf<S extends string> =\n  ParseFunction<S> extends {\n    args: infer Args extends string\n  }\n    ? SplitByComma<Args> extends [string, ...infer Cmds extends string[]]\n      ? Cmds\n      : []\n    : []\n\nexport type CommandCountOf<S extends string> = CommandsOf<S>[\"length\"]\n\n// ---------------------------------------------------------------------------\n// internal state (exported for advanced use)\n// ---------------------------------------------------------------------------\n\nexport interface Point {\n  x: string\n  y: string\n}\n\nexport type ShapeCommand =\n  | { kind: \"move\" | \"line\"; by: boolean; to: Point }\n  | { kind: \"hline\" | \"vline\"; by: boolean; value: string }\n  | { kind: \"curve\"; by: boolean; to: Point; control: Point; control2?: Point }\n  | { kind: \"smooth\"; by: boolean; to: Point; control?: Point }\n  | { kind: \"arc\"; by: boolean; to: Point; radius: Point; flags?: string }\n  | { kind: \"close\" }\n\nexport interface ShapeValue {\n  fillRule: \"nonzero\" | \"evenodd\" | null\n  from: Point\n  commands: ShapeCommand[]\n}\n",
      "type": "registry:ui",
      "target": "components/ui/shape-path-editor/shape-path-editor.types.ts"
    },
    {
      "path": "src/components/ui/shape-path-editor/shape-path-editor.helpers.ts",
      "content": "// =====================================================================\n// shape-path-editor.helpers.ts\n//\n// Pure runtime parse / format / geometry for CSS `shape()` values\n// (CSS Shapes L2; for clip-path / offset-path). This is the SUPERSET of the\n// strict type tier in shape-path-editor.types.ts: it dispatches each comma-\n// separated command on its name (move/line/hline/vline/curve/smooth/arc/\n// close) into the discriminated `ShapeCommand` union, tolerates a lenient arc\n// flags tail (spec A5), and drives the draggable canvas via shapeToPoints /\n// updatePoint. The single source the UI parses from and serializes to.\n// =====================================================================\n\nimport type {\n  Point,\n  ShapeCommand,\n  ShapeCommandName,\n  ShapeValue,\n} from \"./shape-path-editor.types\"\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst COMMAND_NAMES: readonly ShapeCommandName[] = [\n  \"move\",\n  \"line\",\n  \"hline\",\n  \"vline\",\n  \"curve\",\n  \"smooth\",\n  \"arc\",\n  \"close\",\n]\n\nconst COMMAND_NAME_SET = new Set<string>(COMMAND_NAMES)\nconst FILL_RULES = new Set<string>([\"nonzero\", \"evenodd\"])\n\nconst CALL_RE = /^([a-z-]+)\\((.*)\\)$/is\n\n// ---------------------------------------------------------------------------\n// Splitters (paren-aware; runtime mirror of the kit combinators)\n// ---------------------------------------------------------------------------\n\n/** Split on `sep` only at bracket depth 0. */\nfunction splitTopLevel(src: string, sep: string): string[] {\n  const out: string[] = []\n  let depth = 0\n  let cur = \"\"\n  for (const ch of src) {\n    if (ch === \"(\" || ch === \"[\") depth++\n    else if (ch === \")\" || ch === \"]\") depth--\n    if (ch === sep && depth === 0) {\n      out.push(cur)\n      cur = \"\"\n    } else {\n      cur += ch\n    }\n  }\n  out.push(cur)\n  return out\n}\n\n/** Split into space-separated tokens, dropping empty runs. */\nfunction splitSpace(src: string): string[] {\n  return splitTopLevel(src, \" \")\n    .map((s) => s.trim())\n    .filter((s) => s.length > 0)\n}\n\n/** Split into comma-separated parts, dropping empty parts. */\nfunction splitComma(src: string): string[] {\n  return splitTopLevel(src, \",\")\n    .map((s) => s.trim())\n    .filter((s) => s.length > 0)\n}\n\n// ---------------------------------------------------------------------------\n// Coordinate dimension check (runtime mirror of LP<S> — needs a unit)\n// ---------------------------------------------------------------------------\n\nconst LP_RE =\n  /^[+-]?(\\d+\\.?\\d*|\\.\\d+)(%|px|rem|em|vw|vh|vmin|vmax|ch|ex|cm|mm|in|pt|pc|q)$/i\n\n/** A `<length-percentage>` token — a number with a unit (bare `0` rejected). */\nfunction isLP(token: string): boolean {\n  return LP_RE.test(token.trim())\n}\n\nfunction isByTo(token: string): token is \"by\" | \"to\" {\n  return token === \"by\" || token === \"to\"\n}\n\n// ---------------------------------------------------------------------------\n// from seed\n// ---------------------------------------------------------------------------\n\ninterface FromSeed {\n  fillRule: \"nonzero\" | \"evenodd\" | null\n  from: Point\n}\n\nfunction parseFromSeg(seg: string): FromSeed | null {\n  let tokens = splitSpace(seg)\n  let fillRule: \"nonzero\" | \"evenodd\" | null = null\n  if (tokens.length > 0 && FILL_RULES.has(tokens[0])) {\n    fillRule = tokens[0] as \"nonzero\" | \"evenodd\"\n    tokens = tokens.slice(1)\n  }\n  if (tokens.length !== 3 || tokens[0] !== \"from\") return null\n  const [, x, y] = tokens\n  if (!isLP(x) || !isLP(y)) return null\n  return { fillRule, from: { x, y } }\n}\n\n// ---------------------------------------------------------------------------\n// per-command builders — tokens → ShapeCommand | null\n// ---------------------------------------------------------------------------\n\nfunction buildLineLike(\n  kind: \"move\" | \"line\",\n  rest: string[],\n): ShapeCommand | null {\n  if (rest.length !== 3) return null\n  const [dir, x, y] = rest\n  if (!isByTo(dir) || !isLP(x) || !isLP(y)) return null\n  return { kind, by: dir === \"by\", to: { x, y } }\n}\n\nfunction buildHV(kind: \"hline\" | \"vline\", rest: string[]): ShapeCommand | null {\n  if (rest.length !== 2) return null\n  const [dir, value] = rest\n  if (!isByTo(dir) || !isLP(value)) return null\n  return { kind, by: dir === \"by\", value }\n}\n\nfunction buildCurve(rest: string[]): ShapeCommand | null {\n  // <by|to> <x> <y> with <cx> <cy> [ / <cx2> <cy2> ]\n  if (rest.length < 6) return null\n  const [dir, x, y, withKw, ...ctrl] = rest\n  if (!isByTo(dir) || !isLP(x) || !isLP(y) || withKw !== \"with\") return null\n  const to: Point = { x, y }\n  if (ctrl.length === 2) {\n    if (!isLP(ctrl[0]) || !isLP(ctrl[1])) return null\n    return {\n      kind: \"curve\",\n      by: dir === \"by\",\n      to,\n      control: { x: ctrl[0], y: ctrl[1] },\n    }\n  }\n  if (ctrl.length === 5 && ctrl[2] === \"/\") {\n    const [cx1, cy1, , cx2, cy2] = ctrl\n    if (!isLP(cx1) || !isLP(cy1) || !isLP(cx2) || !isLP(cy2)) return null\n    return {\n      kind: \"curve\",\n      by: dir === \"by\",\n      to,\n      control: { x: cx1, y: cy1 },\n      control2: { x: cx2, y: cy2 },\n    }\n  }\n  return null\n}\n\nfunction buildSmooth(rest: string[]): ShapeCommand | null {\n  // <by|to> <x> <y> [ with <cx> <cy> ]\n  const [dir, x, y, withKw, cx, cy] = rest\n  if (rest.length === 3) {\n    if (!isByTo(dir) || !isLP(x) || !isLP(y)) return null\n    return { kind: \"smooth\", by: dir === \"by\", to: { x, y } }\n  }\n  if (rest.length === 6) {\n    if (\n      !isByTo(dir) ||\n      !isLP(x) ||\n      !isLP(y) ||\n      withKw !== \"with\" ||\n      !isLP(cx) ||\n      !isLP(cy)\n    ) {\n      return null\n    }\n    return {\n      kind: \"smooth\",\n      by: dir === \"by\",\n      to: { x, y },\n      control: { x: cx, y: cy },\n    }\n  }\n  return null\n}\n\nfunction buildArc(rest: string[]): ShapeCommand | null {\n  // <by|to> <x> <y> of <rx> [ <ry> ] [ flags… ]  (flags lenient — A5)\n  if (rest.length < 5) return null\n  const [dir, x, y, ofKw, rx, ...tail] = rest\n  if (!isByTo(dir) || !isLP(x) || !isLP(y) || ofKw !== \"of\" || !isLP(rx)) {\n    return null\n  }\n  // A second radius is present iff the next token is also a <length-percentage>.\n  let ry = rx\n  let flagsTokens = tail\n  if (tail.length > 0 && isLP(tail[0])) {\n    ry = tail[0]\n    flagsTokens = tail.slice(1)\n  }\n  const cmd: Extract<ShapeCommand, { kind: \"arc\" }> = {\n    kind: \"arc\",\n    by: dir === \"by\",\n    to: { x, y },\n    radius: { x: rx, y: ry },\n  }\n  if (flagsTokens.length > 0) cmd.flags = flagsTokens.join(\" \")\n  return cmd\n}\n\nfunction buildCommand(seg: string): ShapeCommand | null {\n  const tokens = splitSpace(seg)\n  if (tokens.length === 0) return null\n  const name = tokens[0]\n  const rest = tokens.slice(1)\n  switch (name) {\n    case \"close\":\n      return rest.length === 0 ? { kind: \"close\" } : null\n    case \"move\":\n    case \"line\":\n      return buildLineLike(name, rest)\n    case \"hline\":\n    case \"vline\":\n      return buildHV(name, rest)\n    case \"curve\":\n      return buildCurve(rest)\n    case \"smooth\":\n      return buildSmooth(rest)\n    case \"arc\":\n      return buildArc(rest)\n    default:\n      return null\n  }\n}\n\n// ---------------------------------------------------------------------------\n// parseShape — string → { fillRule, from, commands, error }\n// ---------------------------------------------------------------------------\n\nconst EMPTY_FROM: Point = { x: \"0px\", y: \"0px\" }\n\n/**\n * Parse a CSS `shape()` value into typed state. `error` is `null` on success\n * and a message otherwise; on error `from`/`commands` hold whatever was parsed\n * so far (empty when the wrapper itself is malformed). Rejects a non-`shape()`\n * function, a missing `from` seed, an unknown command, wrong arity, a missing\n * `by`/`to` direction, and any coordinate without a unit.\n */\nexport function parseShape(src: string): ShapeValue & { error: string | null } {\n  const trimmed = src.trim()\n  const m = trimmed.match(CALL_RE)\n  if (m === null || m[1].toLowerCase() !== \"shape\") {\n    return {\n      fillRule: null,\n      from: EMPTY_FROM,\n      commands: [],\n      error: \"not a shape() function\",\n    }\n  }\n  const segs = splitComma(m[2])\n  if (segs.length === 0) {\n    return {\n      fillRule: null,\n      from: EMPTY_FROM,\n      commands: [],\n      error: \"empty shape()\",\n    }\n  }\n  const seed = parseFromSeg(segs[0])\n  if (seed === null) {\n    return {\n      fillRule: null,\n      from: EMPTY_FROM,\n      commands: [],\n      error: \"a shape() must begin with a `from <x> <y>` seed\",\n    }\n  }\n  const commands: ShapeCommand[] = []\n  for (const seg of segs.slice(1)) {\n    const cmd = buildCommand(seg)\n    if (cmd === null) {\n      return {\n        fillRule: seed.fillRule,\n        from: seed.from,\n        commands,\n        error: `invalid command: ${seg}`,\n      }\n    }\n    commands.push(cmd)\n  }\n  return { fillRule: seed.fillRule, from: seed.from, commands, error: null }\n}\n\n// ---------------------------------------------------------------------------\n// formatShape — ShapeValue → canonical string\n// ---------------------------------------------------------------------------\n\nfunction dir(by: boolean): string {\n  return by ? \"by\" : \"to\"\n}\n\nfunction commandToCss(cmd: ShapeCommand): string {\n  switch (cmd.kind) {\n    case \"close\":\n      return \"close\"\n    case \"move\":\n    case \"line\":\n      return `${cmd.kind} ${dir(cmd.by)} ${cmd.to.x} ${cmd.to.y}`\n    case \"hline\":\n    case \"vline\":\n      return `${cmd.kind} ${dir(cmd.by)} ${cmd.value}`\n    case \"curve\": {\n      const tail =\n        cmd.control2 !== undefined\n          ? `${cmd.control.x} ${cmd.control.y} / ${cmd.control2.x} ${cmd.control2.y}`\n          : `${cmd.control.x} ${cmd.control.y}`\n      return `curve ${dir(cmd.by)} ${cmd.to.x} ${cmd.to.y} with ${tail}`\n    }\n    case \"smooth\": {\n      const tail =\n        cmd.control !== undefined\n          ? ` with ${cmd.control.x} ${cmd.control.y}`\n          : \"\"\n      return `smooth ${dir(cmd.by)} ${cmd.to.x} ${cmd.to.y}${tail}`\n    }\n    case \"arc\": {\n      const radius =\n        cmd.radius.x === cmd.radius.y\n          ? cmd.radius.x\n          : `${cmd.radius.x} ${cmd.radius.y}`\n      const flags = cmd.flags !== undefined ? ` ${cmd.flags}` : \"\"\n      return `arc ${dir(cmd.by)} ${cmd.to.x} ${cmd.to.y} of ${radius}${flags}`\n    }\n  }\n}\n\n/** Canonical re-serialization of a `shape()` value (units preserved). */\nexport function formatShape(shape: ShapeValue): string {\n  const lead = shape.fillRule !== null ? `${shape.fillRule} ` : \"\"\n  const head = `${lead}from ${shape.from.x} ${shape.from.y}`\n  const body = shape.commands.map(commandToCss)\n  return `shape(${[head, ...body].join(\", \")})`\n}\n\n// ---------------------------------------------------------------------------\n// commandNames / commandArity / defaultShape\n// ---------------------------------------------------------------------------\n\n/** The eight `<shape-command>` names, in canonical order. */\nexport function commandNames(): readonly ShapeCommandName[] {\n  return COMMAND_NAMES\n}\n\n/** Whether `name` is one of the eight command kinds. */\nexport function isCommandName(name: string): name is ShapeCommandName {\n  return COMMAND_NAME_SET.has(name)\n}\n\n/**\n * The number of draggable coordinate pairs a command kind carries on the\n * canvas: its endpoint plus any control point(s). `hline`/`vline` (a single\n * scalar, no canvas point) and `close` carry none.\n */\nexport function commandArity(kind: ShapeCommandName): number {\n  switch (kind) {\n    case \"move\":\n    case \"line\":\n    case \"smooth\":\n    case \"arc\":\n      return 1\n    case \"curve\":\n      return 2\n    case \"hline\":\n    case \"vline\":\n    case \"close\":\n      return 0\n  }\n}\n\n/** A valid, parseable seed value for a freshly-created editor. */\nexport function defaultShape(): string {\n  return \"shape(from 0px 0px, line to 100px 0px, close)\"\n}\n\n// ---------------------------------------------------------------------------\n// shapeToPoints / updatePoint — the canvas geometry (normalized 0..200 px)\n// ---------------------------------------------------------------------------\n\nexport type PointRole = \"endpoint\" | \"control\" | \"control2\"\n\nexport interface CanvasPoint {\n  /** Stable id derived from cmdIndex + role (survives a re-derive). */\n  id: string\n  role: PointRole\n  /** The owning command index; `-1` is the `from` seed. */\n  cmdIndex: number\n  x: number\n  y: number\n}\n\n/** Strip the unit off a `<length-percentage>`, returning its numeric part. */\nfunction toNumber(value: string): number {\n  const n = Number.parseFloat(value)\n  return Number.isFinite(n) ? n : 0\n}\n\nfunction pointId(cmdIndex: number, role: PointRole): string {\n  return `${cmdIndex}:${role}`\n}\n\n/**\n * Project a shape's endpoints and Bézier control handles into a flat list of\n * draggable canvas points in a normalized 0..200 px space (units stripped).\n * The `from` seed is `cmdIndex: -1`. `hline`/`vline`/`close` contribute no\n * points (a single scalar / no geometry). `curve` contributes its endpoint, a\n * `control`, and — when cubic — a `control2`; `smooth` contributes its\n * endpoint (its optional control is left to the row editor, not the canvas).\n */\nexport function shapeToPoints(shape: ShapeValue): CanvasPoint[] {\n  const out: CanvasPoint[] = [\n    {\n      id: pointId(-1, \"endpoint\"),\n      role: \"endpoint\",\n      cmdIndex: -1,\n      x: toNumber(shape.from.x),\n      y: toNumber(shape.from.y),\n    },\n  ]\n  shape.commands.forEach((cmd, i) => {\n    switch (cmd.kind) {\n      case \"move\":\n      case \"line\":\n      case \"smooth\":\n      case \"arc\":\n        out.push({\n          id: pointId(i, \"endpoint\"),\n          role: \"endpoint\",\n          cmdIndex: i,\n          x: toNumber(cmd.to.x),\n          y: toNumber(cmd.to.y),\n        })\n        break\n      case \"curve\":\n        out.push({\n          id: pointId(i, \"endpoint\"),\n          role: \"endpoint\",\n          cmdIndex: i,\n          x: toNumber(cmd.to.x),\n          y: toNumber(cmd.to.y),\n        })\n        out.push({\n          id: pointId(i, \"control\"),\n          role: \"control\",\n          cmdIndex: i,\n          x: toNumber(cmd.control.x),\n          y: toNumber(cmd.control.y),\n        })\n        if (cmd.control2 !== undefined) {\n          out.push({\n            id: pointId(i, \"control2\"),\n            role: \"control2\",\n            cmdIndex: i,\n            x: toNumber(cmd.control2.x),\n            y: toNumber(cmd.control2.y),\n          })\n        }\n        break\n      // hline / vline / close contribute no draggable canvas point.\n    }\n  })\n  return out\n}\n\nfunction px(n: number): string {\n  return `${n}px`\n}\n\n/**\n * Write a dragged canvas point's `(x, y)` (in px space) back into the matching\n * slot, returning a new `ShapeValue`. An unrecognized id is a no-op. Coordinates\n * are written with `px` units.\n */\nexport function updatePoint(\n  shape: ShapeValue,\n  id: string,\n  x: number,\n  y: number,\n): ShapeValue {\n  if (id === pointId(-1, \"endpoint\")) {\n    return { ...shape, from: { x: px(x), y: px(y) } }\n  }\n  const sep = id.indexOf(\":\")\n  if (sep === -1) return shape\n  const cmdIndex = Number.parseInt(id.slice(0, sep), 10)\n  const role = id.slice(sep + 1) as PointRole\n  if (!Number.isInteger(cmdIndex) || cmdIndex < 0) return shape\n  if (cmdIndex >= shape.commands.length) return shape\n\n  const next = px(x)\n  const nextY = px(y)\n  const commands = shape.commands.map((cmd, i) => {\n    if (i !== cmdIndex) return cmd\n    switch (cmd.kind) {\n      case \"move\":\n      case \"line\":\n      case \"smooth\":\n      case \"arc\":\n        return role === \"endpoint\" ? { ...cmd, to: { x: next, y: nextY } } : cmd\n      case \"curve\":\n        if (role === \"endpoint\") return { ...cmd, to: { x: next, y: nextY } }\n        if (role === \"control\") {\n          return { ...cmd, control: { x: next, y: nextY } }\n        }\n        if (role === \"control2\" && cmd.control2 !== undefined) {\n          return { ...cmd, control2: { x: next, y: nextY } }\n        }\n        return cmd\n      default:\n        return cmd\n    }\n  })\n  return { ...shape, commands }\n}\n",
      "type": "registry:ui",
      "target": "components/ui/shape-path-editor/shape-path-editor.helpers.ts"
    },
    {
      "path": "src/components/ui/shape-path-editor/shape-canvas.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useRef, useState } from \"react\"\nimport { cn } from \"@/lib/utils\"\nimport type { CanvasPoint } from \"./shape-path-editor.helpers\"\nimport {\n  formatShape,\n  parseShape,\n  shapeToPoints,\n  updatePoint,\n} from \"./shape-path-editor.helpers\"\nimport type { ShapeValue } from \"./shape-path-editor.types\"\n\n// ---------------------------------------------------------------------------\n// ShapeCanvas (public) — the draggable SVG path canvas. Mirrors\n// clip-path-editor's draggable-vertex preview, leveled up with Bézier control\n// handles (each with a connector line to its owning endpoint) and an arc\n// radius gizmo. Endpoints + control handles come from `shapeToPoints`; dragging\n// or arrow-nudging one calls `updatePoint` → re-serializes → `onChange`.\n//\n// Coordinates live in a normalized 0..CANVAS px space (the `from` seed plus\n// every command coordinate). The stage maps client px → canvas px via its\n// bounding rect, so a 200px-wide stage and a 200px canvas are 1:1.\n// ---------------------------------------------------------------------------\n\nconst CANVAS = 200\n\nexport interface ShapeCanvasProps {\n  /** The current `shape()` value. */\n  value: string\n  /** Omit for a read-only canvas (no drag handles). */\n  onChange?: (value: string) => void\n  className?: string\n}\n\ninterface DragTarget {\n  id: string\n}\n\n/** Clamp a number into the canvas range. */\nfunction clampCanvas(n: number): number {\n  return Math.max(0, Math.min(CANVAS, n))\n}\n\n/** Round to 1 decimal place, dropping a trailing \".0\". */\nfunction round1(n: number): number {\n  return Math.round(n * 10) / 10\n}\n\n/** Human-readable role label for a handle's aria-label. */\nfunction roleLabel(p: CanvasPoint, kind: string): string {\n  if (p.cmdIndex === -1) return \"from point\"\n  if (p.role === \"control\")\n    return `command ${p.cmdIndex + 1} ${kind} control handle`\n  if (p.role === \"control2\")\n    return `command ${p.cmdIndex + 1} ${kind} second control handle`\n  return `command ${p.cmdIndex + 1} ${kind} endpoint`\n}\n\nexport function ShapeCanvas({ value, onChange, className }: ShapeCanvasProps) {\n  const stageRef = useRef<HTMLDivElement | null>(null)\n  const dragRef = useRef<DragTarget | null>(null)\n  const [dragging, setDragging] = useState(false)\n\n  const parsed = parseShape(value)\n  const shape: ShapeValue = {\n    fillRule: parsed.fillRule,\n    from: parsed.from,\n    commands: parsed.commands,\n  }\n  const points = shapeToPoints(shape)\n\n  // Index endpoints by command so control handles can draw a connector line.\n  const endpointByCmd = new Map<number, CanvasPoint>()\n  for (const p of points) {\n    if (p.role === \"endpoint\") endpointByCmd.set(p.cmdIndex, p)\n  }\n\n  // --- drag math: client coords → canvas px point ------------------------\n  const movePoint = (clientX: number, clientY: number) => {\n    const drag = dragRef.current\n    const stage = stageRef.current\n    if (drag === null || stage === null || !onChange) return\n    const rect = stage.getBoundingClientRect()\n    if (rect.width === 0 || rect.height === 0) return\n    const x = round1(clampCanvas(((clientX - rect.left) / rect.width) * CANVAS))\n    const y = round1(clampCanvas(((clientY - rect.top) / rect.height) * CANVAS))\n    onChange(formatShape(updatePoint(shape, drag.id, x, y)))\n  }\n\n  // Stash the latest closure so the window listeners (attached once per drag)\n  // always call the freshest `movePoint` without re-subscribing each render.\n  const movePointRef = useRef(movePoint)\n  movePointRef.current = movePoint\n\n  useEffect(() => {\n    if (!dragging) return\n    const onPointerMove = (e: PointerEvent) =>\n      movePointRef.current(e.clientX, e.clientY)\n    const onPointerUp = () => {\n      dragRef.current = null\n      setDragging(false)\n    }\n    window.addEventListener(\"pointermove\", onPointerMove)\n    window.addEventListener(\"pointerup\", onPointerUp)\n    return () => {\n      window.removeEventListener(\"pointermove\", onPointerMove)\n      window.removeEventListener(\"pointerup\", onPointerUp)\n    }\n  }, [dragging])\n\n  const nudge = (p: CanvasPoint, dx: number, dy: number) => {\n    if (!onChange) return\n    const nx = round1(clampCanvas(p.x + dx))\n    const ny = round1(clampCanvas(p.y + dy))\n    onChange(formatShape(updatePoint(shape, p.id, nx, ny)))\n  }\n\n  // The outline path: connect endpoints in order (rough preview of the form).\n  const outline = points\n    .filter((p) => p.role === \"endpoint\")\n    .map((p, i) => `${i === 0 ? \"M\" : \"L\"} ${p.x} ${p.y}`)\n    .join(\" \")\n\n  return (\n    <div\n      ref={stageRef}\n      data-testid=\"shape-canvas-stage\"\n      className={cn(\n        \"relative mx-auto aspect-square w-full max-w-[260px] overflow-hidden rounded-md border bg-muted/20\",\n        className,\n      )}\n    >\n      <svg\n        className=\"absolute inset-0 h-full w-full\"\n        aria-hidden=\"true\"\n        viewBox={`0 0 ${CANVAS} ${CANVAS}`}\n        preserveAspectRatio=\"none\"\n      >\n        <title>shape path outline</title>\n        {/* endpoint outline */}\n        {outline ? (\n          <path\n            d={outline}\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeOpacity=\"0.4\"\n            strokeWidth=\"1.2\"\n            vectorEffect=\"non-scaling-stroke\"\n          />\n        ) : null}\n        {/* control-handle connector lines */}\n        {points\n          .filter((p) => p.role === \"control\" || p.role === \"control2\")\n          .map((p) => {\n            const anchor = endpointByCmd.get(p.cmdIndex)\n            if (anchor === undefined) return null\n            return (\n              <line\n                key={`conn-${p.id}`}\n                x1={anchor.x}\n                y1={anchor.y}\n                x2={p.x}\n                y2={p.y}\n                stroke=\"currentColor\"\n                strokeOpacity=\"0.3\"\n                strokeWidth=\"1\"\n                strokeDasharray=\"3 3\"\n                vectorEffect=\"non-scaling-stroke\"\n              />\n            )\n          })}\n      </svg>\n\n      {onChange\n        ? points.map((p) => {\n            const isControl = p.role === \"control\" || p.role === \"control2\"\n            const kind =\n              p.cmdIndex >= 0 ? (shape.commands[p.cmdIndex]?.kind ?? \"\") : \"\"\n            return (\n              <button\n                key={p.id}\n                type=\"button\"\n                aria-label={`${roleLabel(p, kind)} at ${p.x}px ${p.y}px`}\n                onPointerDown={(e) => {\n                  dragRef.current = { id: p.id }\n                  setDragging(true)\n                  if (e.currentTarget.setPointerCapture) {\n                    e.currentTarget.setPointerCapture(e.pointerId)\n                  }\n                }}\n                onKeyDown={(e) => {\n                  const big = e.shiftKey ? 10 : 1\n                  if (e.key === \"ArrowLeft\") {\n                    e.preventDefault()\n                    nudge(p, -big, 0)\n                  } else if (e.key === \"ArrowRight\") {\n                    e.preventDefault()\n                    nudge(p, big, 0)\n                  } else if (e.key === \"ArrowUp\") {\n                    e.preventDefault()\n                    nudge(p, 0, -big)\n                  } else if (e.key === \"ArrowDown\") {\n                    e.preventDefault()\n                    nudge(p, 0, big)\n                  }\n                }}\n                className={cn(\n                  \"absolute -translate-x-1/2 -translate-y-1/2 cursor-grab border-2 shadow focus:outline-none focus:ring-2 focus:ring-ring active:cursor-grabbing\",\n                  isControl\n                    ? \"h-3 w-3 rotate-45 border-amber-400 bg-amber-200\"\n                    : \"h-3.5 w-3.5 rounded-full border-white bg-primary\",\n                )}\n                style={{\n                  left: `${(p.x / CANVAS) * 100}%`,\n                  top: `${(p.y / CANVAS) * 100}%`,\n                }}\n              />\n            )\n          })\n        : null}\n    </div>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/shape-path-editor/shape-canvas.tsx"
    },
    {
      "path": "src/components/ui/shape-path-editor/command-row.tsx",
      "content": "\"use client\"\n\nimport { UnitInput } from \"@/components/ui/unit-input\"\nimport { cn } from \"@/lib/utils\"\nimport { MiniSelect } from \"./mini-select\"\nimport { commandNames } from \"./shape-path-editor.helpers\"\nimport type { Point, ShapeCommand } from \"./shape-path-editor.types\"\n\n// ---------------------------------------------------------------------------\n// CommandRow (public) — one `<shape-command>`'s editor: a command-kind\n// `<select>`, a `by`/`to` direction toggle, and the coordinate / `with` / `of`\n// / flag fields shown per kind. The container owns the `ShapeCommand`; this is\n// presentational. Add/remove/reorder are driven from the container via the\n// `onRemove` / `onMoveUp` / `onMoveDown` callbacks.\n//\n// Coordinates use `unit-input`. A `<length-percentage>` carries its own unit\n// (e.g. `0px` / `0%`); `CoordInput` parses the unit off the value so the\n// scrub + numeric edits round-trip in the original unit.\n// ---------------------------------------------------------------------------\n\n/** Split a `<length-percentage>` into its unit so UnitInput can edit it. */\nfunction unitOf(value: string): string {\n  const m = value.match(/[a-z%]+$/i)\n  return m ? m[0] : \"px\"\n}\n\ninterface CoordInputProps {\n  label: string\n  value: string\n  onChange: (next: string) => void\n}\n\nfunction CoordInput({ label, value, onChange }: CoordInputProps) {\n  const unit = unitOf(value)\n  return (\n    <UnitInput\n      aria-label={label}\n      unit={unit}\n      value={value}\n      onChange={onChange}\n      className=\"w-[96px]\"\n    />\n  )\n}\n\n/** A new endpoint / control point default, keyed to the px canvas space. */\nfunction defaultPoint(): Point {\n  return { x: \"50px\", y: \"50px\" }\n}\n\n/**\n * Reseed a command to a new kind, preserving whatever fields the new kind can\n * carry. Endpoint coordinates carry over; control points / radius get defaults.\n */\nfunction reseedKind(\n  prev: ShapeCommand,\n  kind: ShapeCommand[\"kind\"],\n): ShapeCommand {\n  const by = \"by\" in prev ? prev.by : false\n  const to: Point =\n    \"to\" in prev ? prev.to : \"value\" in prev ? defaultPoint() : defaultPoint()\n  switch (kind) {\n    case \"move\":\n    case \"line\":\n      return { kind, by, to }\n    case \"hline\":\n    case \"vline\":\n      return { kind, by, value: \"50px\" }\n    case \"curve\":\n      return { kind, by, to, control: defaultPoint() }\n    case \"smooth\":\n      return { kind, by, to }\n    case \"arc\":\n      return { kind, by, to, radius: { x: \"50px\", y: \"50px\" } }\n    case \"close\":\n      return { kind: \"close\" }\n  }\n}\n\nexport interface CommandRowProps {\n  index: number\n  command: ShapeCommand\n  onChange: (next: ShapeCommand) => void\n  onRemove: () => void\n  onMoveUp: () => void\n  onMoveDown: () => void\n  canMoveUp: boolean\n  canMoveDown: boolean\n  className?: string\n}\n\nexport function CommandRow({\n  index,\n  command,\n  onChange,\n  onRemove,\n  onMoveUp,\n  onMoveDown,\n  canMoveUp,\n  canMoveDown,\n  className,\n}: CommandRowProps) {\n  const n = index + 1\n  const hasDirection = command.kind !== \"close\"\n\n  return (\n    <div\n      className={cn(\n        \"flex flex-wrap items-center gap-1.5 rounded-md border p-1.5\",\n        className,\n      )}\n    >\n      <span className=\"w-5 text-center font-mono text-[10px] text-muted-foreground\">\n        {n}\n      </span>\n\n      <MiniSelect\n        aria-label={`command ${n} kind`}\n        value={command.kind}\n        onValueChange={(v) =>\n          onChange(reseedKind(command, v as ShapeCommand[\"kind\"]))\n        }\n      >\n        {commandNames().map((name) => (\n          <option key={name} value={name}>\n            {name}\n          </option>\n        ))}\n      </MiniSelect>\n\n      {hasDirection ? (\n        <MiniSelect\n          aria-label={`command ${n} direction`}\n          value={\"by\" in command && command.by ? \"by\" : \"to\"}\n          onValueChange={(v) => onChange({ ...command, by: v === \"by\" })}\n        >\n          <option value=\"to\">to</option>\n          <option value=\"by\">by</option>\n        </MiniSelect>\n      ) : null}\n\n      {(command.kind === \"move\" ||\n        command.kind === \"line\" ||\n        command.kind === \"smooth\") && (\n        <>\n          <CoordInput\n            label={`command ${n} x`}\n            value={command.to.x}\n            onChange={(x) => onChange({ ...command, to: { ...command.to, x } })}\n          />\n          <CoordInput\n            label={`command ${n} y`}\n            value={command.to.y}\n            onChange={(y) => onChange({ ...command, to: { ...command.to, y } })}\n          />\n        </>\n      )}\n\n      {(command.kind === \"hline\" || command.kind === \"vline\") && (\n        <CoordInput\n          label={`command ${n} value`}\n          value={command.value}\n          onChange={(value) => onChange({ ...command, value })}\n        />\n      )}\n\n      {command.kind === \"curve\" && (\n        <>\n          <CoordInput\n            label={`command ${n} x`}\n            value={command.to.x}\n            onChange={(x) => onChange({ ...command, to: { ...command.to, x } })}\n          />\n          <CoordInput\n            label={`command ${n} y`}\n            value={command.to.y}\n            onChange={(y) => onChange({ ...command, to: { ...command.to, y } })}\n          />\n          <span className=\"font-mono text-[10px] text-muted-foreground\">\n            with\n          </span>\n          <CoordInput\n            label={`command ${n} control x`}\n            value={command.control.x}\n            onChange={(x) =>\n              onChange({ ...command, control: { ...command.control, x } })\n            }\n          />\n          <CoordInput\n            label={`command ${n} control y`}\n            value={command.control.y}\n            onChange={(y) =>\n              onChange({ ...command, control: { ...command.control, y } })\n            }\n          />\n        </>\n      )}\n\n      {command.kind === \"arc\" && (\n        <>\n          <CoordInput\n            label={`command ${n} x`}\n            value={command.to.x}\n            onChange={(x) => onChange({ ...command, to: { ...command.to, x } })}\n          />\n          <CoordInput\n            label={`command ${n} y`}\n            value={command.to.y}\n            onChange={(y) => onChange({ ...command, to: { ...command.to, y } })}\n          />\n          <span className=\"font-mono text-[10px] text-muted-foreground\">\n            of\n          </span>\n          <CoordInput\n            label={`command ${n} radius x`}\n            value={command.radius.x}\n            onChange={(x) =>\n              onChange({ ...command, radius: { ...command.radius, x } })\n            }\n          />\n          <CoordInput\n            label={`command ${n} radius y`}\n            value={command.radius.y}\n            onChange={(y) =>\n              onChange({ ...command, radius: { ...command.radius, y } })\n            }\n          />\n        </>\n      )}\n\n      <div className=\"ml-auto flex items-center gap-0.5\">\n        <button\n          type=\"button\"\n          aria-label={`Move command ${n} up`}\n          disabled={!canMoveUp}\n          onClick={onMoveUp}\n          className=\"rounded p-1 text-muted-foreground hover:text-foreground disabled:opacity-30\"\n        >\n          ↑\n        </button>\n        <button\n          type=\"button\"\n          aria-label={`Move command ${n} down`}\n          disabled={!canMoveDown}\n          onClick={onMoveDown}\n          className=\"rounded p-1 text-muted-foreground hover:text-foreground disabled:opacity-30\"\n        >\n          ↓\n        </button>\n        <button\n          type=\"button\"\n          aria-label={`Remove command ${n}`}\n          onClick={onRemove}\n          className=\"rounded p-1 text-muted-foreground hover:text-destructive\"\n        >\n          ×\n        </button>\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/shape-path-editor/command-row.tsx"
    },
    {
      "path": "src/components/ui/shape-path-editor/shape-preview.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useState } from \"react\"\nimport { cn } from \"@/lib/utils\"\nimport { parseShape } from \"./shape-path-editor.helpers\"\nimport type { ShapeCommand, ShapeValue } from \"./shape-path-editor.types\"\n\n// ---------------------------------------------------------------------------\n// ShapePreview (public) — the live `shape()` preview. In `clip-path` mode the\n// produced value clips a gradient box; in `offset-path` mode it animates a dot\n// along the path. Guarded by `CSS.supports(\"clip-path: shape(from 0px 0px)\")`:\n// in a supporting browser (Chrome 137 / Safari 18.4) the live property renders;\n// elsewhere (jsdom, older browsers) it degrades to a raw SVG `<path>` render of\n// the same geometry plus a support note (mirrors `anchor-preview` / `if-function`).\n// ---------------------------------------------------------------------------\n\nconst SUPPORT_FEATURE = \"clip-path: shape(from 0px 0px)\"\n\nfunction detectSupport(): boolean {\n  if (typeof CSS === \"undefined\" || typeof CSS.supports !== \"function\") {\n    return false\n  }\n  try {\n    return CSS.supports(SUPPORT_FEATURE)\n  } catch {\n    return false\n  }\n}\n\n/** Strip the unit off a `<length-percentage>`, returning its numeric part. */\nfunction num(value: string): number {\n  const n = Number.parseFloat(value)\n  return Number.isFinite(n) ? n : 0\n}\n\n/**\n * Build an SVG path `d` from a parsed shape, in the same 0..100 viewBox the\n * fallback draws in. `by` commands accumulate relative to the cursor; `to`\n * commands are absolute. A best-effort sketch — arc flags / smooth reflection\n * are not reconstructed (the degraded path is a hint, not a renderer).\n */\nfunction shapeToPathD(shape: ShapeValue): string {\n  let cx = num(shape.from.x)\n  let cy = num(shape.from.y)\n  const parts: string[] = [`M ${cx} ${cy}`]\n  for (const cmd of shape.commands) {\n    parts.push(...emit(cmd))\n  }\n  function abs(value: string, base: number, rel: boolean): number {\n    return rel ? base + num(value) : num(value)\n  }\n  function emit(cmd: ShapeCommand): string[] {\n    switch (cmd.kind) {\n      case \"move\": {\n        cx = abs(cmd.to.x, cx, cmd.by)\n        cy = abs(cmd.to.y, cy, cmd.by)\n        return [`M ${cx} ${cy}`]\n      }\n      case \"line\": {\n        cx = abs(cmd.to.x, cx, cmd.by)\n        cy = abs(cmd.to.y, cy, cmd.by)\n        return [`L ${cx} ${cy}`]\n      }\n      case \"hline\": {\n        cx = abs(cmd.value, cx, cmd.by)\n        return [`L ${cx} ${cy}`]\n      }\n      case \"vline\": {\n        cy = abs(cmd.value, cy, cmd.by)\n        return [`L ${cx} ${cy}`]\n      }\n      case \"curve\": {\n        const ex = abs(cmd.to.x, cx, cmd.by)\n        const ey = abs(cmd.to.y, cy, cmd.by)\n        const c1x = abs(cmd.control.x, cx, cmd.by)\n        const c1y = abs(cmd.control.y, cy, cmd.by)\n        let out: string\n        if (cmd.control2 !== undefined) {\n          const c2x = abs(cmd.control2.x, cx, cmd.by)\n          const c2y = abs(cmd.control2.y, cy, cmd.by)\n          out = `C ${c1x} ${c1y} ${c2x} ${c2y} ${ex} ${ey}`\n        } else {\n          out = `Q ${c1x} ${c1y} ${ex} ${ey}`\n        }\n        cx = ex\n        cy = ey\n        return [out]\n      }\n      case \"smooth\": {\n        const ex = abs(cmd.to.x, cx, cmd.by)\n        const ey = abs(cmd.to.y, cy, cmd.by)\n        cx = ex\n        cy = ey\n        return [`L ${ex} ${ey}`]\n      }\n      case \"arc\": {\n        const ex = abs(cmd.to.x, cx, cmd.by)\n        const ey = abs(cmd.to.y, cy, cmd.by)\n        const rx = num(cmd.radius.x)\n        const ry = num(cmd.radius.y)\n        cx = ex\n        cy = ey\n        return [`A ${rx} ${ry} 0 0 1 ${ex} ${ey}`]\n      }\n      case \"close\":\n        return [\"Z\"]\n    }\n  }\n  return parts.join(\" \")\n}\n\nexport type ShapePreviewMode = \"clip-path\" | \"offset-path\"\n\nexport interface ShapePreviewProps {\n  /** The `shape()` value to visualize. */\n  value: string\n  /** `\"clip-path\"` (default) clips a box; `\"offset-path\"` animates a dot. */\n  mode?: ShapePreviewMode\n  className?: string\n}\n\nexport function ShapePreview({\n  value,\n  mode = \"clip-path\",\n  className,\n}: ShapePreviewProps) {\n  // Detect once on mount (post-hydration) so SSR + jsdom take the static path.\n  const [supported, setSupported] = useState(false)\n  useEffect(() => {\n    setSupported(detectSupport())\n  }, [])\n\n  const parsed = parseShape(value)\n  const shape: ShapeValue = {\n    fillRule: parsed.fillRule,\n    from: parsed.from,\n    commands: parsed.commands,\n  }\n  const pathD = shapeToPathD(shape)\n  const applied = parsed.error === null ? value : undefined\n\n  return (\n    <div className={cn(\"space-y-2 rounded-lg border p-3\", className)}>\n      <div className=\"flex items-center justify-between\">\n        <span className=\"text-muted-foreground text-xs\">preview · {mode}</span>\n        {supported ? (\n          <span className=\"rounded bg-emerald-500/15 px-2 py-0.5 font-mono text-[10px] text-emerald-400\">\n            shape() ✓\n          </span>\n        ) : (\n          <span className=\"rounded bg-muted px-2 py-0.5 font-mono text-[10px] text-muted-foreground\">\n            not supported\n          </span>\n        )}\n      </div>\n\n      {supported && mode === \"clip-path\" ? (\n        <div className=\"relative mx-auto aspect-square w-full max-w-[260px] overflow-hidden rounded-md bg-[conic-gradient(at_30%_30%,#6366f1,#ec4899,#f59e0b,#10b981,#6366f1)]\">\n          <div\n            data-shape-target\n            className=\"absolute inset-0 bg-[linear-gradient(135deg,#0ea5e9,#8b5cf6,#ec4899)]\"\n            style={{ clipPath: applied }}\n            aria-hidden=\"true\"\n          />\n        </div>\n      ) : supported && mode === \"offset-path\" ? (\n        <div className=\"relative mx-auto aspect-square w-full max-w-[260px] overflow-hidden rounded-md bg-muted/30\">\n          <div\n            data-shape-target\n            className=\"absolute size-4 rounded-full bg-primary\"\n            style={\n              {\n                offsetPath: applied,\n                offsetDistance: \"50%\",\n              } as React.CSSProperties\n            }\n            aria-hidden=\"true\"\n          />\n        </div>\n      ) : (\n        <FallbackPath pathD={pathD} />\n      )}\n\n      <p className=\"text-[10px] text-muted-foreground/70 leading-relaxed\">\n        {supported ? (\n          <>\n            Live render via <code className=\"font-mono\">{mode}: shape(…)</code>.\n          </>\n        ) : (\n          <>\n            CSS <code className=\"font-mono\">shape()</code> is unavailable here —\n            showing a degraded SVG path of the same geometry. The produced value\n            still copies and works in a supporting browser (Chrome 137 / Safari\n            18.4).\n          </>\n        )}\n      </p>\n    </div>\n  )\n}\n\n// ---------------------------------------------------------------------------\n// FallbackPath — the degraded, support-free SVG path render.\n// ---------------------------------------------------------------------------\n\nfunction FallbackPath({ pathD }: { pathD: string }) {\n  return (\n    <div\n      data-testid=\"shape-preview-fallback\"\n      role=\"img\"\n      aria-label=\"Static SVG path render of the shape\"\n      className=\"relative mx-auto aspect-square w-full max-w-[260px] overflow-hidden rounded-md bg-muted/30\"\n    >\n      <svg\n        className=\"absolute inset-0 h-full w-full text-primary\"\n        viewBox=\"0 0 200 200\"\n        preserveAspectRatio=\"xMidYMid meet\"\n        aria-hidden=\"true\"\n      >\n        <title>shape path</title>\n        <path\n          d={pathD}\n          fill=\"currentColor\"\n          fillOpacity=\"0.2\"\n          stroke=\"currentColor\"\n          strokeWidth=\"1.5\"\n          vectorEffect=\"non-scaling-stroke\"\n        />\n      </svg>\n    </div>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/shape-path-editor/shape-preview.tsx"
    },
    {
      "path": "src/components/ui/shape-path-editor/mini-select.tsx",
      "content": "\"use client\"\n\nimport type { ReactNode } from \"react\"\nimport { cn } from \"@/lib/utils\"\n\n// ---------------------------------------------------------------------------\n// MiniSelect — the compact `<select>` chrome shared by the shape-path-editor\n// command rows (command kind, by/to direction, fill-rule). A LOCAL copy, per\n// registry self-containment: every component owns its own mini-select so a\n// `shadcn add shape-path-editor` never reaches into a sibling component's\n// internals. `onValueChange` hands back the raw `e.target.value`.\n// ---------------------------------------------------------------------------\n\nexport const selectClass =\n  \"h-8 rounded-md border border-input bg-background px-1 font-mono text-xs\"\n\nexport interface MiniSelectProps {\n  \"aria-label\": string\n  value: string\n  onValueChange: (value: string) => void\n  children: ReactNode\n  className?: string\n}\n\nexport function MiniSelect({\n  \"aria-label\": ariaLabel,\n  value,\n  onValueChange,\n  children,\n  className,\n}: MiniSelectProps) {\n  return (\n    <select\n      aria-label={ariaLabel}\n      value={value}\n      onChange={(e) => onValueChange(e.target.value)}\n      className={cn(selectClass, className)}\n    >\n      {children}\n    </select>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/shape-path-editor/mini-select.tsx"
    }
  ],
  "type": "registry:ui"
}