{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "clip-path-editor",
  "title": "Clip Path Editor",
  "description": "Ridiculously typed CSS clip-path / shape-outside editor for a single <basic-shape> (inset / circle / ellipse / polygon) with an optional geometry-box keyword, validated by compile-time basic-shape dispatch through ParseFunction. Ships a tolerant runtime parser and a live preview with draggable polygon vertices.",
  "dependencies": [],
  "registryDependencies": [
    "ridiculous-type-kit",
    "unit-input",
    "button",
    "popover",
    "input",
    "label",
    "select"
  ],
  "files": [
    {
      "path": "src/components/ui/clip-path-editor/index.ts",
      "content": "export type {\n  CircleControlsProps,\n  ClipPathEditorPanelProps,\n  ClipPathEditorProps,\n  ClipPathPreviewProps,\n  EllipseControlsProps,\n  GeometryBoxSelectProps,\n  InsetControlsProps,\n  LengthPctEditorProps,\n  PolygonControlsProps,\n  ShapeSelectProps,\n} from \"./clip-path-editor\"\nexport {\n  CircleControls,\n  ClipPathEditor,\n  ClipPathEditorPanel,\n  ClipPathPreview,\n  EllipseControls,\n  GeometryBoxSelect,\n  InsetControls,\n  LengthPctEditor,\n  PolygonControls,\n  ShapeSelect,\n} from \"./clip-path-editor\"\nexport {\n  defaultShape,\n  formatClipPath,\n  parseClipPath,\n  polygonVertices,\n  shapeName,\n  shapeToCss,\n} from \"./clip-path-editor.helpers\"\nexport type {\n  BasicShapeName,\n  ClipPathLiteral,\n  ClipPathShape,\n  ClipPathShapeState,\n  ClipPathState,\n  ClipPathString,\n  ClipPathStringMap,\n  Dimension,\n  GeometryBox,\n  GeometryBoxOf,\n  ShapeOf,\n  VertexCountOf,\n} from \"./clip-path-editor.types\"\nexport { cssClipPath } from \"./clip-path-editor.types\"\n",
      "type": "registry:ui",
      "target": "components/ui/clip-path-editor/index.ts"
    },
    {
      "path": "src/components/ui/clip-path-editor/clip-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 type { ClipMode } from \"./clip-path-editor.constants\"\nimport {\n  defaultShape,\n  formatClipPath,\n  parseClipPath,\n  shapeToCss,\n} from \"./clip-path-editor.helpers\"\nimport type {\n  BasicShapeName,\n  ClipPathShapeState,\n  ClipPathState,\n  ClipPathString,\n  GeometryBox,\n} from \"./clip-path-editor.types\"\nimport { ShapeControls } from \"./controls/shape-controls\"\nimport { ClipPathPreview } from \"./preview/clip-path-preview\"\nimport { GeometryBoxSelect } from \"./primitives/geometry-box-select\"\nimport { LiveString } from \"./primitives/live-string\"\nimport { ShapeSelect } from \"./primitives/shape-select\"\n\n// ---------------------------------------------------------------------------\n// Shared props\n// ---------------------------------------------------------------------------\n\nexport interface ClipPathEditorPanelProps {\n  value: ClipPathString | (string & {})\n  onChange: (value: ClipPathString) => void\n  /**\n   * Which CSS property the live preview targets. Both `clip-path` and\n   * `shape-outside` share the identical basic-shape grammar, so this does NOT\n   * change validation or narrow the `onChange` output — it only drives the\n   * preview render target + labels. Defaults to `\"clip-path\"`.\n   */\n  mode?: ClipMode\n  className?: string\n  \"aria-label\"?: string\n}\n\nexport interface ClipPathEditorProps extends ClipPathEditorPanelProps {}\n\n// ---------------------------------------------------------------------------\n// ClipPathEditor — popover-wrapped\n// ---------------------------------------------------------------------------\n\nexport function ClipPathEditor(props: ClipPathEditorProps) {\n  const {\n    value,\n    className,\n    \"aria-label\": ariaLabel = \"Edit a clip-path\",\n  } = props\n  const state = parseClipPath(String(value))\n  const label =\n    state === null\n      ? \"invalid\"\n      : state.shape === null\n        ? (state.box ?? \"none\")\n        : state.shape.shape\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-[180px] truncate text-xs\">{value}</span>\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent className=\"w-auto p-0\" align=\"start\">\n        <ClipPathEditorPanel {...props} />\n      </PopoverContent>\n    </Popover>\n  )\n}\n\n// ---------------------------------------------------------------------------\n// ClipPathEditorPanel — inline\n// ---------------------------------------------------------------------------\n\nexport function ClipPathEditorPanel({\n  value,\n  onChange,\n  mode = \"clip-path\",\n  className,\n  \"aria-label\": ariaLabel = \"CSS clip-path editor\",\n}: ClipPathEditorPanelProps) {\n  const [state, setState] = useState<ClipPathState>(\n    () => parseClipPath(String(value)) ?? { shape: null },\n  )\n  const lastEmittedRef = useRef<string | null>(null)\n\n  // Resync from external value (skip our own emits).\n  useEffect(() => {\n    if (value === lastEmittedRef.current) return\n    const parsed = parseClipPath(String(value))\n    if (parsed !== null) setState(parsed)\n  }, [value])\n\n  const commit = (next: ClipPathState) => {\n    setState(next)\n    const str = formatClipPath(next)\n    lastEmittedRef.current = str\n    onChange(str as ClipPathString)\n  }\n\n  const setShape = (shape: ClipPathShapeState | null) => {\n    commit({ ...state, shape })\n  }\n\n  const changeShapeKind = (kind: BasicShapeName) => {\n    commit({ ...state, shape: defaultShape(kind) })\n  }\n\n  const setBox = (box: GeometryBox | undefined) => {\n    if (box === undefined) {\n      const { box: _drop, boxPosition: _dropPos, ...rest } = state\n      commit(rest)\n    } else {\n      commit({\n        ...state,\n        box,\n        boxPosition: state.boxPosition ?? \"trailing\",\n      })\n    }\n  }\n\n  const setBoxPosition = (position: \"leading\" | \"trailing\") => {\n    if (state.box === undefined) return\n    commit({ ...state, boxPosition: position })\n  }\n\n  const currentShapeKind = state.shape?.shape\n\n  return (\n    <fieldset\n      className={cn(\n        \"m-0 w-[480px] space-y-3 border-0 bg-background p-3\",\n        className,\n      )}\n      aria-label={ariaLabel}\n    >\n      <div className=\"flex flex-wrap items-center gap-2\">\n        <ShapeSelect\n          value={currentShapeKind}\n          onChange={(kind) => {\n            if (kind === \"none\") setShape(null)\n            else changeShapeKind(kind)\n          }}\n        />\n        <GeometryBoxSelect value={state.box} onChange={setBox} />\n        {state.box !== undefined ? (\n          <select\n            aria-label=\"Box position\"\n            value={state.boxPosition ?? \"trailing\"}\n            onChange={(e) =>\n              setBoxPosition(e.target.value as \"leading\" | \"trailing\")\n            }\n            className=\"h-8 rounded border bg-background px-1.5 font-mono text-xs\"\n          >\n            <option value=\"leading\">leading</option>\n            <option value=\"trailing\">trailing</option>\n          </select>\n        ) : null}\n      </div>\n\n      {state.shape !== null ? (\n        <div className=\"rounded-md border p-2\">\n          <ShapeControls shape={state.shape} onChange={setShape} />\n        </div>\n      ) : (\n        <p className=\"rounded-md border border-dashed p-3 text-center text-muted-foreground text-xs\">\n          {state.box !== undefined\n            ? \"A bare geometry box. Pick a shape to add a basic-shape function.\"\n            : \"No shape (none). Pick a basic shape above.\"}\n        </p>\n      )}\n\n      <LiveString value={formatClipPath(state)} />\n\n      <ClipPathPreview\n        value={formatClipPath(state)}\n        mode={mode}\n        onChange={(str) => {\n          const parsed = parseClipPath(str)\n          if (parsed !== null) commit(parsed)\n        }}\n      />\n    </fieldset>\n  )\n}\n\n// ---------------------------------------------------------------------------\n// Public re-exports — these named exports keep\n// `@/components/ui/clip-path-editor/clip-path-editor` import-compatible.\n// ---------------------------------------------------------------------------\n\nexport type { CircleControlsProps } from \"./controls/circle\"\nexport { CircleControls } from \"./controls/circle\"\nexport type { EllipseControlsProps } from \"./controls/ellipse\"\nexport { EllipseControls } from \"./controls/ellipse\"\nexport type { InsetControlsProps } from \"./controls/inset\"\nexport { InsetControls } from \"./controls/inset\"\nexport type { LengthPctEditorProps } from \"./controls/length-pct\"\nexport { LengthPctEditor } from \"./controls/length-pct\"\nexport type { PolygonControlsProps } from \"./controls/polygon\"\nexport { PolygonControls } from \"./controls/polygon\"\nexport type { ClipPathPreviewProps } from \"./preview/clip-path-preview\"\nexport { ClipPathPreview } from \"./preview/clip-path-preview\"\nexport type { GeometryBoxSelectProps } from \"./primitives/geometry-box-select\"\nexport { GeometryBoxSelect } from \"./primitives/geometry-box-select\"\nexport type { ShapeSelectProps } from \"./primitives/shape-select\"\nexport { ShapeSelect } from \"./primitives/shape-select\"\n\n// Keep `shapeToCss` reachable as a named import for advanced consumers.\nexport { shapeToCss }\n",
      "type": "registry:ui",
      "target": "components/ui/clip-path-editor/clip-path-editor.tsx"
    },
    {
      "path": "src/components/ui/clip-path-editor/clip-path-editor.types.ts",
      "content": "// =====================================================================\n// clip-path-editor.types.ts\n//\n// The \"ridiculous\" tier: compile-time BASIC-SHAPE DISPATCH over a CSS\n// `clip-path` / `shape-outside` value. The value is a single\n// `<basic-shape>` — inset() / circle() / ellipse() / polygon() — with an\n// optional leading OR trailing `<geometry-box>` keyword. Built on\n// `ridiculous-type-kit`. The strict validator `ClipPathLiteral<S>`\n// resolves to `S` when the shape (its arity + each argument's DIMENSION)\n// validates, `never` otherwise.\n//\n//   \"circle(50% at center)\"            →  the literal\n//   \"inset(45deg)\"                     →  never (wants length-percentage)\n//   \"polygon(0% 0%, 100% 0%, 50%)\"     →  never (odd-token vertex)\n//   \"circle(50%) border-box\"           →  the literal (trailing box)\n//   \"none\"                             →  \"none\"\n//\n// Unlike transform/filter (a space-separated function LIST), clip-path is\n// ONE function, so dispatch is a single ParseFunction on the value after\n// peeling an optional geometry box. The variadic challenge lives in\n// polygon()'s vertex list, capped at 32 vertices (the tail is weak-\n// validated beyond the cap — see VERTEX_CAP).\n// =====================================================================\n\nimport type {\n  And,\n  IsLength,\n  IsPercentage,\n  Or,\n  ParseFunction,\n  SplitByComma,\n  SplitBySpace,\n  Trim,\n} from \"@/lib/ridiculous-type-kit\"\n\n// =====================================================================\n// 1. PRIMITIVE PREDICATE ALIASES\n// =====================================================================\n\n/** length OR percentage — the universal clip-path coordinate dimension. */\ntype IsLengthPct<S extends string> = Or<\n  IsLength<Trim<S>>,\n  IsPercentage<Trim<S>>\n>\n\n/** A circle/ellipse radius: a length-% OR a sizing keyword. */\ntype IsRadius<S extends string> = Or<\n  IsLengthPct<S>,\n  Trim<S> extends \"closest-side\" | \"farthest-side\" ? true : false\n>\n\n/** A single `<position>` token: an edge keyword OR a length-%. */\ntype IsPositionToken<S extends string> = Or<\n  Trim<S> extends \"left\" | \"right\" | \"center\" | \"top\" | \"bottom\" ? true : false,\n  IsLengthPct<S>\n>\n\n// =====================================================================\n// 2. POSITION (simplified — 0, 1, or 2 tokens; see spec §2.4)\n//    Edge-offset 3/4-token forms resolve to false here (the runtime\n//    parser accepts them). The editor only ever emits 1/2-token forms.\n// =====================================================================\n\ntype ValidatePosition<Toks extends string[]> = Toks extends []\n  ? true\n  : Toks extends [infer A extends string]\n    ? IsPositionToken<A>\n    : Toks extends [infer A extends string, infer B extends string]\n      ? And<IsPositionToken<A>, IsPositionToken<B>>\n      : false\n\n// =====================================================================\n// 3. SHAPE ARGUMENT VALIDATORS\n// =====================================================================\n\n// --- inset( <lp>{1,4} [ round <radius> ]? ) --------------------------\n// Peel an optional `round <tail>`; the box must be 1-4 length-%; a present\n// `round` requires a non-empty tail (weak-validated — full border-radius\n// grammar is out of scope, see spec §2.1).\n\ntype AllLengthPct<A extends string[]> = A extends [\n  infer H extends string,\n  ...infer T extends string[],\n]\n  ? IsLengthPct<H> extends true\n    ? AllLengthPct<T>\n    : false\n  : true\n\n/** Split a token list at the first `round`; `{ box; round; tail }`. */\ntype SplitRound<\n  Toks extends string[],\n  Box extends string[] = [],\n> = Toks extends [infer H extends string, ...infer T extends string[]]\n  ? Trim<H> extends \"round\"\n    ? { box: Box; round: true; tail: T }\n    : SplitRound<T, [...Box, H]>\n  : { box: Box; round: false; tail: [] }\n\ntype ValidateInset<ArgStr extends string> =\n  SplitBySpace<ArgStr> extends infer Toks extends string[]\n    ? SplitRound<Toks> extends {\n        box: infer Box extends string[]\n        round: infer R extends boolean\n        tail: infer Tail extends string[]\n      }\n      ? Box[\"length\"] extends 1 | 2 | 3 | 4\n        ? And<\n            AllLengthPct<Box>,\n            R extends true ? (Tail extends [] ? false : true) : true\n          >\n        : false\n      : false\n    : false\n\n// --- circle( <radius>? [ at <position> ]? ) --------------------------\n// First token (if not `at`) is the radius; an `at` keyword starts the\n// position clause.\n\ntype ValidateCircle<ArgStr extends string> =\n  Trim<ArgStr> extends \"\"\n    ? true\n    : SplitBySpace<ArgStr> extends infer Toks extends string[]\n      ? Toks extends [infer H extends string, ...infer T extends string[]]\n        ? Trim<H> extends \"at\"\n          ? ValidatePosition<T>\n          : T extends [infer A extends string, ...infer Rest extends string[]]\n            ? Trim<A> extends \"at\"\n              ? And<IsRadius<H>, ValidatePosition<Rest>>\n              : false // a second non-`at` token → too many radii for a circle\n            : IsRadius<H> // single radius, no position\n        : false\n      : false\n\n// --- ellipse( [ <radius> <radius> ]? [ at <position> ]? ) ------------\n// Zero radii OR exactly two, optionally followed by `at <position>`.\n\ntype ValidateEllipse<ArgStr extends string> =\n  Trim<ArgStr> extends \"\"\n    ? true\n    : SplitBySpace<ArgStr> extends infer Toks extends string[]\n      ? Toks extends [infer A extends string, ...infer T extends string[]]\n        ? Trim<A> extends \"at\"\n          ? false // ellipse needs two radii before `at` (0-radii uses empty args)\n          : T extends [infer B extends string, ...infer Rest extends string[]]\n            ? Trim<B> extends \"at\"\n              ? false // only one radius before `at`\n              : And<\n                  And<IsRadius<A>, IsRadius<B>>,\n                  Rest extends [\n                    infer C extends string,\n                    ...infer Pos extends string[],\n                  ]\n                    ? Trim<C> extends \"at\"\n                      ? ValidatePosition<Pos>\n                      : false // a third radius\n                    : true // exactly two radii, no position\n                >\n            : false // only one radius total\n        : false\n      : false\n\n// --- polygon( [ <fill-rule> , ]? <vertex># ) ------------------------\n// Comma-separated vertices; each vertex is two length-% (space-separated).\n// VARIADIC — validated per-vertex up to VERTEX_CAP, then weak-validated.\n\n/** Depth cap on per-vertex validation (spec §2.5 / §7). */\ntype VERTEX_CAP = 32\n\ntype ValidateVertex<S extends string> =\n  SplitBySpace<Trim<S>> extends [infer X extends string, infer Y extends string]\n    ? And<IsLengthPct<X>, IsLengthPct<Y>>\n    : false\n\ntype ValidateVertices<\n  V extends string[],\n  Depth extends unknown[] = [],\n> = V extends [infer H extends string, ...infer T extends string[]]\n  ? Depth[\"length\"] extends VERTEX_CAP\n    ? true // cap reached — weak-validate the remaining tail\n    : ValidateVertex<H> extends true\n      ? ValidateVertices<T, [...Depth, unknown]>\n      : false\n  : true\n\ntype ValidatePolygon<ArgStr extends string> =\n  SplitByComma<ArgStr> extends infer Parts extends string[]\n    ? Parts extends [infer First extends string, ...infer Rest extends string[]]\n      ? Trim<First> extends \"nonzero\" | \"evenodd\"\n        ? Rest extends [] // a fill-rule but no vertices\n          ? false\n          : ValidateVertices<Rest>\n        : ValidateVertices<Parts>\n      : false // empty\n    : false\n\n// =====================================================================\n// 4. SHAPE DISPATCH — ParseFunction → per-shape validator → boolean.\n// =====================================================================\n\ntype ValidateShape<\n  ArgStr extends string,\n  Name extends string,\n> = Name extends \"inset\"\n  ? ValidateInset<ArgStr>\n  : Name extends \"circle\"\n    ? ValidateCircle<ArgStr>\n    : Name extends \"ellipse\"\n      ? ValidateEllipse<ArgStr>\n      : Name extends \"polygon\"\n        ? ValidatePolygon<ArgStr>\n        : false // unknown shape\n\n/** Validate a bare shape function (no geometry box), → true | false. */\ntype ValidateBareShape<S extends string> =\n  ParseFunction<Trim<S>> extends {\n    name: infer Name extends string\n    args: infer ArgStr extends string\n  }\n    ? ValidateShape<ArgStr, Name>\n    : false\n\n// =====================================================================\n// 4b. GEOMETRY-BOX PEEL — right/left-anchored, at most one box.\n//\n// A plain `${infer Box} ${infer Rest}` infers leftmost, so it mis-splits\n// `circle(50% at center) border-box` (the box has spaces). Distributing\n// over the `GeometryBox` union with a FIXED suffix/prefix literal makes TS\n// anchor the match to that exact box token, peeling correctly.\n// =====================================================================\n\n/** `{ box; rest }` peeling an optional leading/trailing box; `box: \"none\"`\n *  when there's no box (or a box appears at both ends → marked invalid). */\ntype StripLeadingBox<S extends string> = S extends `${infer B} ${infer Rest}`\n  ? B extends GeometryBox\n    ? { box: B; rest: Rest; position: \"leading\" }\n    : { box: \"none\"; rest: S; position: \"none\" }\n  : { box: \"none\"; rest: S; position: \"none\" }\n\ntype StripTrailingBox<S extends string> = GeometryBox extends infer B\n  ? B extends string\n    ? S extends `${infer Rest} ${B}`\n      ? { box: B; rest: Rest; position: \"trailing\" }\n      : never\n    : never\n  : never\n\n/** Peel one box (trailing preferred — it is unambiguous), else leading. */\ntype PeelBox<S extends string> = [StripTrailingBox<S>] extends [never]\n  ? StripLeadingBox<S>\n  : StripTrailingBox<S>\n\n// =====================================================================\n// 5. STRICT VALIDATOR + CALL-SITE HELPER\n//    Peel an optional leading OR trailing geometry box (at most one),\n//    then validate the remaining shape. A bare box is valid on its own.\n// =====================================================================\n\n/**\n * Strict literal validator. Resolves to `S` when `S` is a dimensionally-\n * and arity-valid CSS `clip-path` / `shape-outside` value — a single\n * basic shape with an optional leading OR trailing geometry box, a bare\n * geometry box, or the `none` keyword — and `never` otherwise. `calc()` /\n * `var()` inside an argument resolve to `never` here (undecidable at\n * compile time) — use the casual / IntelliSense tier; the runtime parser\n * accepts them.\n *\n * @example\n * type A = ClipPathLiteral<\"circle(50% at center)\">       // the literal\n * type B = ClipPathLiteral<\"inset(45deg)\">                // never\n * type C = ClipPathLiteral<\"circle(50%) border-box\">      // the literal\n * type D = ClipPathLiteral<\"none\">                        // \"none\"\n */\nexport type ClipPathLiteral<S extends string> =\n  Trim<S> extends \"none\"\n    ? S\n    : Trim<S> extends \"\"\n      ? never\n      : // bare geometry box on its own\n        Trim<S> extends GeometryBox\n        ? S\n        : PeelBox<Trim<S>> extends {\n              box: infer Box\n              rest: infer Rest extends string\n            }\n          ? Box extends \"none\"\n            ? // no box — validate the whole value as a shape\n              ValidateBareShape<Trim<S>> extends true\n              ? S\n              : never\n            : // one box peeled — `Rest` must be a boxless shape (this rejects\n              // a SECOND box, since a bare box is not a valid ParseFunction)\n              ValidateBareShape<Rest> extends true\n              ? S\n              : never\n          : never\n\n/**\n * Call-site validator helper. Mirrors `cssFilter()` / `cssTransform()` /\n * `cssCalc()` / `color()` / `easing()`. An invalid clip-path becomes a\n * type error at the argument.\n */\nexport const cssClipPath = <S extends string>(\n  value: S & ClipPathLiteral<S>,\n): S => value\n\n// =====================================================================\n// 6. SUGGESTION STRINGS — IntelliSense + onChange return types\n// =====================================================================\n\n/** The supported basic-shape function names. */\nexport type BasicShapeName = \"inset\" | \"circle\" | \"ellipse\" | \"polygon\"\n\n/** The CSS `<geometry-box>` keywords. */\nexport type GeometryBox =\n  | \"margin-box\"\n  | \"border-box\"\n  | \"padding-box\"\n  | \"content-box\"\n  | \"fill-box\"\n  | \"stroke-box\"\n  | \"view-box\"\n\n/**\n * Suggestion union — \"this is a clip-path string\". Per-shape heads, with an\n * optional leading or trailing geometry box, plus a bare box and `none`.\n */\nexport type ClipPathString =\n  | `${BasicShapeName}(${string})`\n  | `${BasicShapeName}(${string}) ${GeometryBox}`\n  | `${GeometryBox} ${BasicShapeName}(${string})`\n  | GeometryBox\n  | \"none\"\n\n/** Shape → output-string map. Backs the per-shape suggestion shapes. */\nexport interface ClipPathStringMap {\n  inset: `inset(${string})`\n  circle: `circle(${string})`\n  ellipse: `ellipse(${string})`\n  polygon: `polygon(${string})`\n}\n\nexport type ClipPathShape = keyof ClipPathStringMap\n\n// =====================================================================\n// 7. UTILITY TYPES — operate on clip-path literals at the type level\n// =====================================================================\n\n/** Strip a leading/trailing geometry box, returning the inner shape text. */\ntype StripBox<S extends string> =\n  Trim<S> extends GeometryBox\n    ? \"\" // bare box → no shape\n    : PeelBox<Trim<S>> extends { rest: infer Rest extends string }\n      ? Rest\n      : Trim<S>\n\n/**\n * The basic shape of a clip-path value: the shape name, `\"box\"` for a bare\n * geometry box, or `\"none\"`.\n *\n * @example\n * type A = ShapeOf<\"circle(50%)\">              // \"circle\"\n * type B = ShapeOf<\"ellipse(1px 2px) border-box\"> // \"ellipse\"\n * type C = ShapeOf<\"border-box\">               // \"box\"\n * type D = ShapeOf<\"none\">                     // \"none\"\n */\nexport type ShapeOf<S extends string> =\n  Trim<S> extends \"none\"\n    ? \"none\"\n    : Trim<S> extends GeometryBox\n      ? \"box\"\n      : ParseFunction<StripBox<S>> extends {\n            name: infer Name extends BasicShapeName\n          }\n        ? Name\n        : never\n\n/** Drop a leading fill-rule from a polygon arg's comma parts. */\ntype DropFillRule<Parts extends string[]> = Parts extends [\n  infer First extends string,\n  ...infer Rest extends string[],\n]\n  ? Trim<First> extends \"nonzero\" | \"evenodd\"\n    ? Rest\n    : Parts\n  : Parts\n\n/**\n * The number of vertices in a `polygon()` value (0 if not a polygon).\n *\n * @example\n * type A = VertexCountOf<\"polygon(0% 0%, 100% 0%, 50% 100%)\"> // 3\n * type B = VertexCountOf<\"nonzero polygon...\">                // (fill-rule dropped)\n * type C = VertexCountOf<\"circle(50%)\">                       // 0\n */\nexport type VertexCountOf<S extends string> =\n  ShapeOf<S> extends \"polygon\"\n    ? ParseFunction<StripBox<S>> extends { args: infer ArgStr extends string }\n      ? DropFillRule<SplitByComma<ArgStr>>[\"length\"]\n      : 0\n    : 0\n\n/**\n * The geometry-box keyword present in a clip-path value, or `\"none\"`.\n *\n * @example\n * type A = GeometryBoxOf<\"circle(50%) border-box\">    // \"border-box\"\n * type B = GeometryBoxOf<\"padding-box ellipse(1px 2px)\"> // \"padding-box\"\n * type C = GeometryBoxOf<\"circle(50%)\">               // \"none\"\n */\nexport type GeometryBoxOf<S extends string> =\n  Trim<S> extends GeometryBox\n    ? Trim<S>\n    : PeelBox<Trim<S>> extends { box: infer Box extends GeometryBox | \"none\" }\n      ? Box\n      : \"none\"\n\n// =====================================================================\n// 8. INTERNAL STATE — discriminated union (exported)\n//\n// The editor's state is a single shape (discriminated by `shape`) plus an\n// optional geometry box. Exported for advanced use (custom serialization,\n// programmatic build). Argument values are kept as strings (they carry\n// units), mirroring how the literal preserves the raw text.\n// =====================================================================\n\nexport type ClipPathShapeState =\n  // 1-4 length-% box (right/bottom/left optional via CSS shorthand) + round\n  | {\n      shape: \"inset\"\n      top: string\n      right?: string\n      bottom?: string\n      left?: string\n      round?: string\n    }\n  // one radius (length-% or keyword) + optional `at` position\n  | { shape: \"circle\"; radius?: string; atX?: string; atY?: string }\n  // two radii + optional `at` position\n  | { shape: \"ellipse\"; rx?: string; ry?: string; atX?: string; atY?: string }\n  // optional fill-rule + a vertex list\n  | {\n      shape: \"polygon\"\n      fillRule?: \"nonzero\" | \"evenodd\"\n      vertices: Array<{ x: string; y: string }>\n    }\n\nexport interface ClipPathState {\n  /** The optional geometry-box keyword. */\n  box?: GeometryBox\n  /** Where the box sits relative to the shape (for round-trip fidelity). */\n  boxPosition?: \"leading\" | \"trailing\"\n  /** The basic shape, or `null` for a bare geometry box / `none`. */\n  shape: ClipPathShapeState | null\n}\n\n// Re-export the kit's Dimension for convenience.\nexport type { Dimension } from \"@/lib/ridiculous-type-kit\"\n",
      "type": "registry:ui",
      "target": "components/ui/clip-path-editor/clip-path-editor.types.ts"
    },
    {
      "path": "src/components/ui/clip-path-editor/clip-path-editor.helpers.ts",
      "content": "// =====================================================================\n// clip-path-editor.helpers.ts\n//\n// Pure runtime parse / format / spec for CSS `clip-path` / `shape-outside`\n// values. This is the SUPERSET of the strict type tier: it tolerates\n// calc()/var() inside coordinates (kept verbatim, opaque), accepts a\n// leading OR trailing geometry box, validates arity, and drives the UI\n// from a single shape-spec dispatch.\n// =====================================================================\n\nimport type {\n  BasicShapeName,\n  ClipPathShapeState,\n  ClipPathState,\n  GeometryBox,\n} from \"./clip-path-editor.types\"\n\n// ---------------------------------------------------------------------------\n// Constants — geometry boxes, shape names, fill rules\n// ---------------------------------------------------------------------------\n\nconst GEOMETRY_BOXES: readonly GeometryBox[] = [\n  \"margin-box\",\n  \"border-box\",\n  \"padding-box\",\n  \"content-box\",\n  \"fill-box\",\n  \"stroke-box\",\n  \"view-box\",\n]\n\nconst GEOMETRY_BOX_SET = new Set<string>(GEOMETRY_BOXES)\nconst SHAPE_NAMES = new Set<string>([\"inset\", \"circle\", \"ellipse\", \"polygon\"])\n\nfunction isGeometryBox(token: string): token is GeometryBox {\n  return GEOMETRY_BOX_SET.has(token)\n}\n\nfunction isShapeName(name: string): name is BasicShapeName {\n  return SHAPE_NAMES.has(name)\n}\n\n// ---------------------------------------------------------------------------\n// Top-level 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// Name regex allows nothing but letters (inset/circle/ellipse/polygon).\nconst CALL_RE = /^([a-z]+)\\((.*)\\)$/is\n\n// ---------------------------------------------------------------------------\n// Geometry-box peel (mirror of the type-level PeelBox)\n// ---------------------------------------------------------------------------\n\ninterface BoxPeel {\n  box?: GeometryBox\n  boxPosition?: \"leading\" | \"trailing\"\n  rest: string\n  /** true when a box appears at BOTH ends — caller treats as invalid. */\n  doubleBox: boolean\n}\n\n/** Peel at most one geometry box (trailing preferred, then leading). */\nfunction peelBox(src: string): BoxPeel {\n  const tokens = splitSpace(src)\n  if (tokens.length === 0) return { rest: src, doubleBox: false }\n\n  const first = tokens[0]\n  const last = tokens[tokens.length - 1]\n  const leading = tokens.length > 1 && isGeometryBox(first)\n  const trailing = tokens.length > 1 && isGeometryBox(last)\n\n  if (leading && trailing) return { rest: src, doubleBox: true }\n  if (trailing) {\n    return {\n      box: last as GeometryBox,\n      boxPosition: \"trailing\",\n      rest: tokens.slice(0, -1).join(\" \"),\n      doubleBox: false,\n    }\n  }\n  if (leading) {\n    return {\n      box: first as GeometryBox,\n      boxPosition: \"leading\",\n      rest: tokens.slice(1).join(\" \"),\n      doubleBox: false,\n    }\n  }\n  return { rest: src, doubleBox: false }\n}\n\n// ---------------------------------------------------------------------------\n// Per-shape builders — string args → ClipPathShapeState | null\n// ---------------------------------------------------------------------------\n\nfunction buildInset(argStr: string): ClipPathShapeState | null {\n  const tokens = splitSpace(argStr)\n  if (tokens.length === 0) return null\n  const roundIdx = tokens.indexOf(\"round\")\n  const box = roundIdx === -1 ? tokens : tokens.slice(0, roundIdx)\n  const round = roundIdx === -1 ? undefined : tokens.slice(roundIdx + 1)\n  if (box.length < 1 || box.length > 4) return null\n  if (round !== undefined && round.length === 0) return null\n  const [top, right, bottom, left] = box\n  const state: Extract<ClipPathShapeState, { shape: \"inset\" }> = {\n    shape: \"inset\",\n    top,\n  }\n  if (right !== undefined) state.right = right\n  if (bottom !== undefined) state.bottom = bottom\n  if (left !== undefined) state.left = left\n  if (round !== undefined) state.round = round.join(\" \")\n  return state\n}\n\nfunction buildCircle(argStr: string): ClipPathShapeState | null {\n  if (argStr.trim() === \"\") return { shape: \"circle\" }\n  const tokens = splitSpace(argStr)\n  const state: Extract<ClipPathShapeState, { shape: \"circle\" }> = {\n    shape: \"circle\",\n  }\n  const atIdx = tokens.indexOf(\"at\")\n  const radiusTokens = atIdx === -1 ? tokens : tokens.slice(0, atIdx)\n  if (radiusTokens.length > 1) return null // a circle has one radius\n  if (radiusTokens.length === 1) state.radius = radiusTokens[0]\n  if (atIdx !== -1) {\n    const pos = readPosition(tokens.slice(atIdx + 1))\n    if (pos === null) return null\n    state.atX = pos.x\n    state.atY = pos.y\n  }\n  return state\n}\n\nfunction buildEllipse(argStr: string): ClipPathShapeState | null {\n  if (argStr.trim() === \"\") return { shape: \"ellipse\" }\n  const tokens = splitSpace(argStr)\n  const state: Extract<ClipPathShapeState, { shape: \"ellipse\" }> = {\n    shape: \"ellipse\",\n  }\n  const atIdx = tokens.indexOf(\"at\")\n  const radiusTokens = atIdx === -1 ? tokens : tokens.slice(0, atIdx)\n  if (radiusTokens.length !== 2) return null // ellipse needs exactly two radii\n  state.rx = radiusTokens[0]\n  state.ry = radiusTokens[1]\n  if (atIdx !== -1) {\n    const pos = readPosition(tokens.slice(atIdx + 1))\n    if (pos === null) return null\n    state.atX = pos.x\n    state.atY = pos.y\n  }\n  return state\n}\n\nfunction buildPolygon(argStr: string): ClipPathShapeState | null {\n  const parts = splitComma(argStr)\n  if (parts.length === 0) return null\n  const state: Extract<ClipPathShapeState, { shape: \"polygon\" }> = {\n    shape: \"polygon\",\n    vertices: [],\n  }\n  let vertexParts = parts\n  if (parts[0] === \"nonzero\" || parts[0] === \"evenodd\") {\n    state.fillRule = parts[0]\n    vertexParts = parts.slice(1)\n  }\n  if (vertexParts.length === 0) return null\n  for (const part of vertexParts) {\n    const coords = splitSpace(part)\n    if (coords.length !== 2) return null\n    state.vertices.push({ x: coords[0], y: coords[1] })\n  }\n  return state\n}\n\n/**\n * Best-effort `<position>` reader. Handles the 1- and 2-token forms the\n * editor produces; for 3/4-token edge-offset forms it keeps the two values\n * it can represent (dropping edge keywords it cannot — documented).\n */\nfunction readPosition(tokens: string[]): { x: string; y: string } | null {\n  if (tokens.length === 0) return null\n  if (tokens.length === 1) return { x: tokens[0], y: tokens[0] }\n  if (tokens.length === 2) return { x: tokens[0], y: tokens[1] }\n  // 3/4-token edge-offset form: best-effort — take the numeric/keyword values\n  // that are not edge anchors. Fall back to first + last.\n  const values = tokens.filter(\n    (t) => t !== \"left\" && t !== \"right\" && t !== \"top\" && t !== \"bottom\",\n  )\n  if (values.length >= 2) return { x: values[0], y: values[1] }\n  return { x: tokens[0], y: tokens[tokens.length - 1] }\n}\n\nfunction buildShape(\n  name: BasicShapeName,\n  argStr: string,\n): ClipPathShapeState | null {\n  switch (name) {\n    case \"inset\":\n      return buildInset(argStr)\n    case \"circle\":\n      return buildCircle(argStr)\n    case \"ellipse\":\n      return buildEllipse(argStr)\n    case \"polygon\":\n      return buildPolygon(argStr)\n  }\n}\n\n// ---------------------------------------------------------------------------\n// parseClipPath — string → ClipPathState | null\n// ---------------------------------------------------------------------------\n\n/**\n * Parse a CSS `clip-path` / `shape-outside` value into typed state, or `null`\n * on any syntax, unknown-shape, arity, or double-box error. `none` / empty →\n * `{ shape: null }`. A bare geometry box → `{ box, shape: null }`.\n */\nexport function parseClipPath(src: string): ClipPathState | null {\n  const trimmed = src.trim()\n  if (trimmed === \"\" || trimmed === \"none\") return { shape: null }\n\n  // A bare geometry box on its own (single token, no shape).\n  if (isGeometryBox(trimmed)) return { box: trimmed, shape: null }\n\n  const peel = peelBox(trimmed)\n  if (peel.doubleBox) return null\n\n  const rest = peel.rest.trim()\n  const boxFields =\n    peel.box !== undefined\n      ? { box: peel.box, boxPosition: peel.boxPosition }\n      : {}\n\n  // A bare geometry box with no shape.\n  if (rest === \"\") {\n    if (peel.box === undefined) return null\n    return { box: peel.box, shape: null }\n  }\n\n  const m = rest.match(CALL_RE)\n  if (m === null) return null\n  const name = m[1].toLowerCase()\n  if (!isShapeName(name)) return null\n  const shape = buildShape(name, m[2])\n  if (shape === null) return null\n  return { ...boxFields, shape }\n}\n\n// ---------------------------------------------------------------------------\n// formatClipPath — ClipPathState → canonical string\n// ---------------------------------------------------------------------------\n\nfunction insetToCss(\n  s: Extract<ClipPathShapeState, { shape: \"inset\" }>,\n): string {\n  const box = [s.top, s.right, s.bottom, s.left].filter(\n    (v): v is string => v !== undefined,\n  )\n  const round = s.round !== undefined ? ` round ${s.round}` : \"\"\n  return `inset(${box.join(\" \")}${round})`\n}\n\nfunction circleToCss(\n  s: Extract<ClipPathShapeState, { shape: \"circle\" }>,\n): string {\n  const parts: string[] = []\n  if (s.radius !== undefined) parts.push(s.radius)\n  if (s.atX !== undefined && s.atY !== undefined) {\n    parts.push(\"at\", s.atX, s.atY)\n  }\n  return `circle(${parts.join(\" \")})`\n}\n\nfunction ellipseToCss(\n  s: Extract<ClipPathShapeState, { shape: \"ellipse\" }>,\n): string {\n  const parts: string[] = []\n  if (s.rx !== undefined && s.ry !== undefined) parts.push(s.rx, s.ry)\n  if (s.atX !== undefined && s.atY !== undefined) {\n    parts.push(\"at\", s.atX, s.atY)\n  }\n  return `ellipse(${parts.join(\" \")})`\n}\n\nfunction polygonToCss(\n  s: Extract<ClipPathShapeState, { shape: \"polygon\" }>,\n): string {\n  const verts = s.vertices.map((v) => `${v.x} ${v.y}`)\n  const body =\n    s.fillRule !== undefined\n      ? [s.fillRule, ...verts].join(\", \")\n      : verts.join(\", \")\n  return `polygon(${body})`\n}\n\n/** Serialize one shape to its CSS function string. */\nexport function shapeToCss(shape: ClipPathShapeState): string {\n  switch (shape.shape) {\n    case \"inset\":\n      return insetToCss(shape)\n    case \"circle\":\n      return circleToCss(shape)\n    case \"ellipse\":\n      return ellipseToCss(shape)\n    case \"polygon\":\n      return polygonToCss(shape)\n  }\n}\n\n/**\n * Canonical re-serialization of a clip-path value. A `null` shape with no box\n * → `none`; a bare box → the keyword; a shape with a box places it per\n * `boxPosition` (default trailing).\n */\nexport function formatClipPath(state: ClipPathState): string {\n  if (state.shape === null) {\n    return state.box ?? \"none\"\n  }\n  const shapeCss = shapeToCss(state.shape)\n  if (state.box === undefined) return shapeCss\n  return state.boxPosition === \"leading\"\n    ? `${state.box} ${shapeCss}`\n    : `${shapeCss} ${state.box}`\n}\n\n// ---------------------------------------------------------------------------\n// defaultShape / shapeName / polygonVertices\n// ---------------------------------------------------------------------------\n\n/** A sensible default state for a freshly-selected shape. */\nexport function defaultShape(shape: BasicShapeName): ClipPathShapeState {\n  switch (shape) {\n    case \"inset\":\n      return {\n        shape: \"inset\",\n        top: \"10%\",\n        right: \"10%\",\n        bottom: \"10%\",\n        left: \"10%\",\n      }\n    case \"circle\":\n      return { shape: \"circle\", radius: \"50%\", atX: \"50%\", atY: \"50%\" }\n    case \"ellipse\":\n      return { shape: \"ellipse\", rx: \"50%\", ry: \"35%\", atX: \"50%\", atY: \"50%\" }\n    case \"polygon\":\n      return {\n        shape: \"polygon\",\n        vertices: [\n          { x: \"50%\", y: \"0%\" },\n          { x: \"0%\", y: \"100%\" },\n          { x: \"100%\", y: \"100%\" },\n        ],\n      }\n  }\n}\n\n/** Runtime mirror of `ShapeOf` — the shape name, `\"box\"`, or `\"none\"`. */\nexport function shapeName(src: string): string {\n  const state = parseClipPath(src)\n  if (state === null) return \"none\"\n  if (state.shape === null) return state.box !== undefined ? \"box\" : \"none\"\n  return state.shape.shape\n}\n\n/** Runtime mirror of the polygon vertex extraction — `[]` if not a polygon. */\nexport function polygonVertices(src: string): Array<{ x: string; y: string }> {\n  const state = parseClipPath(src)\n  if (\n    state === null ||\n    state.shape === null ||\n    state.shape.shape !== \"polygon\"\n  ) {\n    return []\n  }\n  return state.shape.vertices\n}\n",
      "type": "registry:ui",
      "target": "components/ui/clip-path-editor/clip-path-editor.helpers.ts"
    },
    {
      "path": "src/components/ui/clip-path-editor/clip-path-editor.constants.ts",
      "content": "import type { BasicShapeName, GeometryBox } from \"./clip-path-editor.types\"\n\n// ---------------------------------------------------------------------------\n// Shared constants for the clip-path editor UI\n// ---------------------------------------------------------------------------\n\nexport const SHAPES: readonly BasicShapeName[] = [\n  \"inset\",\n  \"circle\",\n  \"ellipse\",\n  \"polygon\",\n]\n\nexport const GEOMETRY_BOXES: readonly GeometryBox[] = [\n  \"margin-box\",\n  \"border-box\",\n  \"padding-box\",\n  \"content-box\",\n  \"fill-box\",\n  \"stroke-box\",\n  \"view-box\",\n]\n\nexport const LP_UNITS = [\"%\", \"px\", \"rem\", \"em\", \"vw\", \"vh\"] as const\n\nexport const RADIUS_KEYWORDS = [\"closest-side\", \"farthest-side\"] as const\n\n/**\n * Which CSS property the live preview targets. Both `clip-path` and\n * `shape-outside` share the identical basic-shape grammar.\n */\nexport type ClipMode = \"clip-path\" | \"shape-outside\"\n",
      "type": "registry:ui",
      "target": "components/ui/clip-path-editor/clip-path-editor.constants.ts"
    },
    {
      "path": "src/components/ui/clip-path-editor/clip-path-editor.hooks.ts",
      "content": "import { useRef } from \"react\"\n\n/**\n * Stable React keys for a positional list whose item shape is serialized and\n * therefore cannot carry an `id` of its own (e.g. polygon `vertices`, which\n * round-trip through parse/format as bare `{ x, y }`).\n *\n * Each slot gets a UUID generated when the slot first appears. The id array is\n * reconciled to `count` on every render: new tail slots get a fresh id, and a\n * shrunk list drops trailing ids. For mid-list removals call `removeIdAt`\n * BEFORE the list shrinks so the surviving slots keep their ids (otherwise a\n * removal would shift every id after the gap, the exact remount hazard a\n * stable key is meant to avoid).\n */\nexport interface ListIds {\n  /** Current ids, one per slot, index-aligned with the data array. */\n  ids: readonly string[]\n  /** Drop the id at `index` so a mid-list removal stays item-stable. */\n  removeIdAt: (index: number) => void\n}\n\nfunction freshId(): string {\n  // `crypto.randomUUID` is available in every browser target + jsdom/node 19+.\n  // Guard for exotic runtimes without it.\n  if (typeof crypto !== \"undefined\" && \"randomUUID\" in crypto) {\n    return crypto.randomUUID()\n  }\n  return `id-${Math.random().toString(36).slice(2)}-${Date.now()}`\n}\n\nexport function useListIds(count: number): ListIds {\n  const idsRef = useRef<string[]>([])\n  const ids = idsRef.current\n\n  // Reconcile length: grow with fresh ids, shrink by truncation.\n  if (ids.length < count) {\n    while (ids.length < count) ids.push(freshId())\n  } else if (ids.length > count) {\n    ids.length = count\n  }\n\n  const removeIdAt = (index: number) => {\n    if (index >= 0 && index < idsRef.current.length) {\n      idsRef.current.splice(index, 1)\n    }\n  }\n\n  return { ids, removeIdAt }\n}\n",
      "type": "registry:ui",
      "target": "components/ui/clip-path-editor/clip-path-editor.hooks.ts"
    },
    {
      "path": "src/components/ui/clip-path-editor/controls/circle.tsx",
      "content": "import type { ClipPathShapeState } from \"../clip-path-editor.types\"\nimport { LabeledField } from \"../primitives/labeled-field\"\nimport { PositionControls } from \"./position\"\nimport { RadiusEditor } from \"./radius\"\n\nexport interface CircleControlsProps {\n  state: Extract<ClipPathShapeState, { shape: \"circle\" }>\n  onChange: (state: ClipPathShapeState) => void\n}\n\nexport function CircleControls({ state, onChange }: CircleControlsProps) {\n  return (\n    <div className=\"flex flex-wrap items-center gap-3\">\n      <LabeledField label=\"radius\">\n        <RadiusEditor\n          label=\"circle radius\"\n          value={state.radius ?? \"50%\"}\n          onChange={(radius) => onChange({ ...state, radius })}\n        />\n      </LabeledField>\n      <LabeledField label=\"position\">\n        <PositionControls\n          atX={state.atX}\n          atY={state.atY}\n          onChange={(atX, atY) => onChange({ ...state, atX, atY })}\n        />\n      </LabeledField>\n    </div>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/clip-path-editor/controls/circle.tsx"
    },
    {
      "path": "src/components/ui/clip-path-editor/controls/ellipse.tsx",
      "content": "import type { ClipPathShapeState } from \"../clip-path-editor.types\"\nimport { LabeledField } from \"../primitives/labeled-field\"\nimport { PositionControls } from \"./position\"\nimport { RadiusEditor } from \"./radius\"\n\nexport interface EllipseControlsProps {\n  state: Extract<ClipPathShapeState, { shape: \"ellipse\" }>\n  onChange: (state: ClipPathShapeState) => void\n}\n\nexport function EllipseControls({ state, onChange }: EllipseControlsProps) {\n  return (\n    <div className=\"flex flex-wrap items-center gap-3\">\n      <LabeledField label=\"radius x\">\n        <RadiusEditor\n          label=\"ellipse radius x\"\n          value={state.rx ?? \"50%\"}\n          onChange={(rx) => onChange({ ...state, rx })}\n        />\n      </LabeledField>\n      <LabeledField label=\"radius y\">\n        <RadiusEditor\n          label=\"ellipse radius y\"\n          value={state.ry ?? \"35%\"}\n          onChange={(ry) => onChange({ ...state, ry })}\n        />\n      </LabeledField>\n      <LabeledField label=\"position\">\n        <PositionControls\n          atX={state.atX}\n          atY={state.atY}\n          onChange={(atX, atY) => onChange({ ...state, atX, atY })}\n        />\n      </LabeledField>\n    </div>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/clip-path-editor/controls/ellipse.tsx"
    },
    {
      "path": "src/components/ui/clip-path-editor/controls/inset.tsx",
      "content": "import { Input } from \"@/components/ui/input\"\nimport type { ClipPathShapeState } from \"../clip-path-editor.types\"\nimport { LabeledField } from \"../primitives/labeled-field\"\nimport { LengthPctEditor } from \"./length-pct\"\n\nexport interface InsetControlsProps {\n  state: Extract<ClipPathShapeState, { shape: \"inset\" }>\n  onChange: (state: ClipPathShapeState) => void\n}\n\nexport function InsetControls({ state, onChange }: InsetControlsProps) {\n  const set = (patch: Partial<typeof state>) => onChange({ ...state, ...patch })\n  return (\n    <div className=\"space-y-2\">\n      <div className=\"grid grid-cols-2 gap-2\">\n        <LabeledField label=\"top\">\n          <LengthPctEditor\n            label=\"inset top\"\n            value={state.top}\n            onChange={(top) => set({ top })}\n          />\n        </LabeledField>\n        <LabeledField label=\"right\">\n          <LengthPctEditor\n            label=\"inset right\"\n            value={state.right ?? \"\"}\n            onChange={(right) =>\n              set({ right: right === \"\" ? undefined : right })\n            }\n          />\n        </LabeledField>\n        <LabeledField label=\"bottom\">\n          <LengthPctEditor\n            label=\"inset bottom\"\n            value={state.bottom ?? \"\"}\n            onChange={(bottom) =>\n              set({ bottom: bottom === \"\" ? undefined : bottom })\n            }\n          />\n        </LabeledField>\n        <LabeledField label=\"left\">\n          <LengthPctEditor\n            label=\"inset left\"\n            value={state.left ?? \"\"}\n            onChange={(left) => set({ left: left === \"\" ? undefined : left })}\n          />\n        </LabeledField>\n      </div>\n      <LabeledField label=\"round\">\n        {state.round === undefined ? (\n          <button\n            type=\"button\"\n            aria-label=\"Add round radius\"\n            onClick={() => set({ round: \"8px\" })}\n            className=\"h-8 rounded border border-dashed px-2 font-mono text-[10px] text-muted-foreground\"\n          >\n            + round\n          </button>\n        ) : (\n          <span className=\"inline-flex items-center gap-1\">\n            <Input\n              aria-label=\"inset round radius\"\n              value={state.round}\n              spellCheck={false}\n              autoComplete=\"off\"\n              onChange={(e) => set({ round: e.target.value })}\n              className=\"h-8 w-[140px] font-mono text-xs\"\n            />\n            <button\n              type=\"button\"\n              aria-label=\"Remove round radius\"\n              onClick={() => set({ round: undefined })}\n              className=\"rounded p-0.5 text-[10px] text-muted-foreground hover:text-destructive\"\n            >\n              ×\n            </button>\n          </span>\n        )}\n      </LabeledField>\n    </div>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/clip-path-editor/controls/inset.tsx"
    },
    {
      "path": "src/components/ui/clip-path-editor/controls/length-pct.tsx",
      "content": "import { Input } from \"@/components/ui/input\"\nimport { cn } from \"@/lib/utils\"\nimport { LP_UNITS } from \"../clip-path-editor.constants\"\n\nexport interface LengthPctEditorProps {\n  label: string\n  value: string\n  onChange: (value: string) => void\n  className?: string\n}\n\n/**\n * A number + unit-select editor for a CSS length-percentage. Values that are\n * not a recognizable `<number><lp-unit>` — `calc()`/`var()` or a bare keyword\n * like `auto`/`fit-content` — fall back to an opaque text input that preserves\n * the value verbatim (no unit coercion, no out-of-range controlled select).\n */\nexport function LengthPctEditor({\n  label,\n  value,\n  onChange,\n  className,\n}: LengthPctEditorProps) {\n  const m = value.match(/^(-?\\d*\\.?\\d*)([a-z%]*)$/i)\n  const numPart = m ? m[1] : value\n  const unitPart = m ? m[2] : \"\"\n  // Opaque (raw text) when the value is NOT a recognizable number + LP unit:\n  // calc()/var() (no regex match), or a bare keyword like `auto` whose \"unit\"\n  // is non-empty but not in LP_UNITS. An empty unit (e.g. \"\" or \"0\") stays a\n  // unit select so a typed bare number gets a \"%\" suffix.\n  const opaque =\n    m === null ||\n    (unitPart !== \"\" && !(LP_UNITS as readonly string[]).includes(unitPart))\n\n  if (opaque) {\n    return (\n      <Input\n        aria-label={label}\n        value={value}\n        spellCheck={false}\n        autoComplete=\"off\"\n        onChange={(e) => onChange(e.target.value)}\n        className={cn(\"h-8 w-[130px] font-mono text-xs\", className)}\n      />\n    )\n  }\n\n  return (\n    <span className={cn(\"inline-flex items-center\", className)}>\n      <Input\n        aria-label={label}\n        value={numPart}\n        spellCheck={false}\n        autoComplete=\"off\"\n        onChange={(e) => onChange(`${e.target.value}${unitPart || \"%\"}`)}\n        className=\"h-8 w-[68px] rounded-r-none border-r-0 font-mono text-xs\"\n      />\n      <select\n        aria-label={`${label} unit`}\n        value={unitPart || \"%\"}\n        onChange={(e) => onChange(`${numPart || \"0\"}${e.target.value}`)}\n        className=\"h-8 rounded-r-md rounded-l-none border border-input bg-background px-1 font-mono text-xs\"\n      >\n        {LP_UNITS.map((u) => (\n          <option key={u} value={u}>\n            {u}\n          </option>\n        ))}\n      </select>\n    </span>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/clip-path-editor/controls/length-pct.tsx"
    },
    {
      "path": "src/components/ui/clip-path-editor/controls/polygon.tsx",
      "content": "import { useListIds } from \"../clip-path-editor.hooks\"\nimport type { ClipPathShapeState } from \"../clip-path-editor.types\"\nimport { LengthPctEditor } from \"./length-pct\"\n\nexport interface PolygonControlsProps {\n  state: Extract<ClipPathShapeState, { shape: \"polygon\" }>\n  onChange: (state: ClipPathShapeState) => void\n}\n\nexport function PolygonControls({ state, onChange }: PolygonControlsProps) {\n  const { vertices } = state\n  const { ids, removeIdAt } = useListIds(vertices.length)\n  const setVertices = (next: Array<{ x: string; y: string }>) =>\n    onChange({ ...state, vertices: next })\n\n  const updateVertex = (index: number, x: string, y: string) => {\n    setVertices(vertices.map((v, i) => (i === index ? { x, y } : v)))\n  }\n  const addVertex = () => {\n    setVertices([...vertices, { x: \"50%\", y: \"50%\" }])\n  }\n  const removeVertex = (index: number) => {\n    if (vertices.length <= 3) return\n    // Drop the matching id first so survivors keep their stable keys.\n    removeIdAt(index)\n    setVertices(vertices.filter((_, i) => i !== index))\n  }\n\n  return (\n    <div className=\"space-y-2\">\n      <div className=\"flex items-center gap-2\">\n        <span className=\"font-mono text-[10px] text-muted-foreground\">\n          fill-rule\n        </span>\n        <select\n          aria-label=\"Fill rule\"\n          value={state.fillRule ?? \"\"}\n          onChange={(e) =>\n            onChange({\n              ...state,\n              fillRule:\n                e.target.value === \"\"\n                  ? undefined\n                  : (e.target.value as \"nonzero\" | \"evenodd\"),\n            })\n          }\n          className=\"h-8 rounded border bg-background px-1.5 font-mono text-xs\"\n        >\n          <option value=\"\">(default)</option>\n          <option value=\"nonzero\">nonzero</option>\n          <option value=\"evenodd\">evenodd</option>\n        </select>\n      </div>\n      <div className=\"max-h-[180px] space-y-1.5 overflow-y-auto pr-1\">\n        {vertices.map((v, i) => (\n          <div key={ids[i]} className=\"flex items-center gap-1.5\">\n            <span className=\"w-6 font-mono text-[10px] text-muted-foreground\">\n              {i + 1}\n            </span>\n            <LengthPctEditor\n              label={`vertex ${i + 1} x`}\n              value={v.x}\n              onChange={(x) => updateVertex(i, x, v.y)}\n            />\n            <LengthPctEditor\n              label={`vertex ${i + 1} y`}\n              value={v.y}\n              onChange={(y) => updateVertex(i, v.x, y)}\n            />\n            <button\n              type=\"button\"\n              aria-label={`Remove vertex ${i + 1}`}\n              disabled={vertices.length <= 3}\n              onClick={() => removeVertex(i)}\n              className=\"rounded p-1 text-muted-foreground hover:text-destructive disabled:opacity-30\"\n            >\n              ×\n            </button>\n          </div>\n        ))}\n      </div>\n      <button\n        type=\"button\"\n        aria-label=\"Add vertex\"\n        onClick={addVertex}\n        className=\"h-8 w-full rounded border border-dashed font-mono text-[10px] text-muted-foreground\"\n      >\n        + add vertex\n      </button>\n    </div>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/clip-path-editor/controls/polygon.tsx"
    },
    {
      "path": "src/components/ui/clip-path-editor/controls/position.tsx",
      "content": "import { LengthPctEditor } from \"./length-pct\"\n\n/** An optional `at <x> <y>` position pair, addable/removable. */\nexport function PositionControls({\n  atX,\n  atY,\n  onChange,\n}: {\n  atX?: string\n  atY?: string\n  onChange: (atX: string | undefined, atY: string | undefined) => void\n}) {\n  const has = atX !== undefined && atY !== undefined\n  if (!has) {\n    return (\n      <button\n        type=\"button\"\n        aria-label=\"Add position\"\n        onClick={() => onChange(\"50%\", \"50%\")}\n        className=\"h-8 rounded border border-dashed px-2 font-mono text-[10px] text-muted-foreground\"\n      >\n        + at position\n      </button>\n    )\n  }\n  return (\n    <span className=\"inline-flex items-center gap-1\">\n      <span className=\"font-mono text-[10px] text-muted-foreground\">at</span>\n      <LengthPctEditor\n        label=\"position x\"\n        value={atX ?? \"50%\"}\n        onChange={(x) => onChange(x, atY)}\n      />\n      <LengthPctEditor\n        label=\"position y\"\n        value={atY ?? \"50%\"}\n        onChange={(y) => onChange(atX, y)}\n      />\n      <button\n        type=\"button\"\n        aria-label=\"Remove position\"\n        onClick={() => onChange(undefined, undefined)}\n        className=\"rounded p-0.5 text-[10px] text-muted-foreground hover:text-destructive\"\n      >\n        ×\n      </button>\n    </span>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/clip-path-editor/controls/position.tsx"
    },
    {
      "path": "src/components/ui/clip-path-editor/controls/radius.tsx",
      "content": "import { RADIUS_KEYWORDS } from \"../clip-path-editor.constants\"\nimport { LengthPctEditor } from \"./length-pct\"\n\n/** A length-% editor OR a sizing keyword (`closest-side`/`farthest-side`). */\nexport function RadiusEditor({\n  label,\n  value,\n  onChange,\n}: {\n  label: string\n  value: string\n  onChange: (value: string) => void\n}) {\n  const isKeyword = (RADIUS_KEYWORDS as readonly string[]).includes(value)\n  return (\n    <span className=\"inline-flex items-center gap-1\">\n      {isKeyword ? (\n        <select\n          aria-label={label}\n          value={value}\n          onChange={(e) => onChange(e.target.value)}\n          className=\"h-8 rounded border bg-background px-1 font-mono text-xs\"\n        >\n          {RADIUS_KEYWORDS.map((k) => (\n            <option key={k} value={k}>\n              {k}\n            </option>\n          ))}\n        </select>\n      ) : (\n        <LengthPctEditor label={label} value={value} onChange={onChange} />\n      )}\n      <button\n        type=\"button\"\n        aria-label={`${label} toggle keyword`}\n        onClick={() => onChange(isKeyword ? \"50%\" : \"closest-side\")}\n        className=\"rounded border px-1 py-0.5 font-mono text-[10px] text-muted-foreground hover:bg-muted\"\n      >\n        {isKeyword ? \"lp\" : \"kw\"}\n      </button>\n    </span>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/clip-path-editor/controls/radius.tsx"
    },
    {
      "path": "src/components/ui/clip-path-editor/controls/shape-controls.tsx",
      "content": "import type { ClipPathShapeState } from \"../clip-path-editor.types\"\nimport { CircleControls } from \"./circle\"\nimport { EllipseControls } from \"./ellipse\"\nimport { InsetControls } from \"./inset\"\nimport { PolygonControls } from \"./polygon\"\n\ninterface ShapeControlsProps {\n  shape: ClipPathShapeState\n  onChange: (shape: ClipPathShapeState) => void\n}\n\n/** Dispatch to the right per-shape control set. */\nexport function ShapeControls({ shape, onChange }: ShapeControlsProps) {\n  switch (shape.shape) {\n    case \"inset\":\n      return <InsetControls state={shape} onChange={onChange} />\n    case \"circle\":\n      return <CircleControls state={shape} onChange={onChange} />\n    case \"ellipse\":\n      return <EllipseControls state={shape} onChange={onChange} />\n    case \"polygon\":\n      return <PolygonControls state={shape} onChange={onChange} />\n  }\n}\n",
      "type": "registry:ui",
      "target": "components/ui/clip-path-editor/controls/shape-controls.tsx"
    },
    {
      "path": "src/components/ui/clip-path-editor/preview/circle-scrubs.tsx",
      "content": "import { UnitInput } from \"@/components/ui/unit-input\"\nimport { formatClipPath } from \"../clip-path-editor.helpers\"\nimport type { ClipPathState } from \"../clip-path-editor.types\"\n\n/** Radius scrub for circle/ellipse using UnitInput (percent radii only). */\nexport function CircleScrubs({\n  state,\n  onChange,\n  id,\n}: {\n  state: ClipPathState | null\n  onChange?: (value: string) => void\n  id: string\n}) {\n  if (\n    state === null ||\n    state.shape === null ||\n    !onChange ||\n    (state.shape.shape !== \"circle\" && state.shape.shape !== \"ellipse\")\n  ) {\n    return null\n  }\n  const shape = state.shape\n\n  if (shape.shape === \"circle\") {\n    const radius = shape.radius ?? \"50%\"\n    // Only scrub a percentage radius (the common case); keyword/length stays as-is.\n    if (!radius.endsWith(\"%\")) return null\n    return (\n      <label\n        htmlFor={`${id}-r`}\n        className=\"flex items-center gap-2 text-muted-foreground text-xs\"\n      >\n        <span className=\"w-16 font-mono\">radius</span>\n        <UnitInput\n          aria-label=\"circle radius scrub\"\n          unit=\"%\"\n          min={0}\n          max={100}\n          value={radius as `${number}%`}\n          onChange={(r) =>\n            onChange(\n              formatClipPath({ ...state, shape: { ...shape, radius: r } }),\n            )\n          }\n        />\n      </label>\n    )\n  }\n\n  // ellipse: scrub rx + ry when both are percentages\n  const rx = shape.rx ?? \"50%\"\n  const ry = shape.ry ?? \"35%\"\n  if (!rx.endsWith(\"%\") || !ry.endsWith(\"%\")) return null\n  return (\n    <div className=\"space-y-1.5\">\n      <label\n        htmlFor={`${id}-rx`}\n        className=\"flex items-center gap-2 text-muted-foreground text-xs\"\n      >\n        <span className=\"w-16 font-mono\">radius x</span>\n        <UnitInput\n          aria-label=\"ellipse radius x scrub\"\n          unit=\"%\"\n          min={0}\n          max={100}\n          value={rx as `${number}%`}\n          onChange={(v) =>\n            onChange(formatClipPath({ ...state, shape: { ...shape, rx: v } }))\n          }\n        />\n      </label>\n      <label\n        htmlFor={`${id}-ry`}\n        className=\"flex items-center gap-2 text-muted-foreground text-xs\"\n      >\n        <span className=\"w-16 font-mono\">radius y</span>\n        <UnitInput\n          aria-label=\"ellipse radius y scrub\"\n          unit=\"%\"\n          min={0}\n          max={100}\n          value={ry as `${number}%`}\n          onChange={(v) =>\n            onChange(formatClipPath({ ...state, shape: { ...shape, ry: v } }))\n          }\n        />\n      </label>\n    </div>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/clip-path-editor/preview/circle-scrubs.tsx"
    },
    {
      "path": "src/components/ui/clip-path-editor/preview/clip-path-preview.tsx",
      "content": "import { useEffect, useId, useRef, useState } from \"react\"\nimport { cn } from \"@/lib/utils\"\nimport type { ClipMode } from \"../clip-path-editor.constants\"\nimport { formatClipPath, parseClipPath } from \"../clip-path-editor.helpers\"\nimport { useListIds } from \"../clip-path-editor.hooks\"\nimport { CircleScrubs } from \"./circle-scrubs\"\n\nexport interface ClipPathPreviewProps {\n  value: string\n  mode?: ClipMode\n  onChange?: (value: string) => void\n  className?: string\n}\n\n/** Round to 1 decimal place, dropping a trailing \".0\". */\nfunction pct(n: number): string {\n  const clamped = Math.max(0, Math.min(100, n))\n  const rounded = Math.round(clamped * 10) / 10\n  return `${rounded}%`\n}\n\nexport function ClipPathPreview({\n  value,\n  mode: modeProp = \"clip-path\",\n  onChange,\n  className,\n}: ClipPathPreviewProps) {\n  const id = useId()\n  const [mode, setMode] = useState<ClipMode>(modeProp)\n  useEffect(() => setMode(modeProp), [modeProp])\n\n  const stageRef = useRef<HTMLDivElement | null>(null)\n  const dragRef = useRef<{ index: number } | null>(null)\n  const [dragging, setDragging] = useState(false)\n\n  const state = parseClipPath(value)\n  const polygon =\n    state !== null && state.shape !== null && state.shape.shape === \"polygon\"\n      ? state.shape\n      : null\n  // Stable keys for the drag handles. Vertex count only changes via the\n  // controls (not drag/nudge), so reconciling by length is item-stable here.\n  const { ids: vertexIds } = useListIds(polygon?.vertices.length ?? 0)\n\n  const applied = value === \"none\" || state === null ? undefined : value\n  const styleProp =\n    mode === \"clip-path\"\n      ? { clipPath: applied }\n      : { shapeOutside: applied, float: \"left\" as const }\n\n  // --- drag math: client coords → percentage vertex ----------------------\n  const moveVertex = (clientX: number, clientY: number) => {\n    const drag = dragRef.current\n    const stage = stageRef.current\n    if (drag === null || stage === null || polygon === null || !onChange) return\n    const rect = stage.getBoundingClientRect()\n    if (rect.width === 0 || rect.height === 0) return\n    const x = pct(((clientX - rect.left) / rect.width) * 100)\n    const y = pct(((clientY - rect.top) / rect.height) * 100)\n    const next = {\n      ...polygon,\n      vertices: polygon.vertices.map((v, i) =>\n        i === drag.index ? { x, y } : v,\n      ),\n    }\n    onChange(formatClipPath({ ...state, shape: next }))\n  }\n\n  // `moveVertex` closes over the latest value/state/polygon every render. We\n  // stash it in a ref so the window listeners (attached ONCE per drag) always\n  // call the freshest closure without re-subscribing on every render tick.\n  const moveVertexRef = useRef(moveVertex)\n  moveVertexRef.current = moveVertex\n\n  // Subscribe to window pointer events only WHILE dragging. Gating on the\n  // `dragging` state means we attach the listeners on pointer-down and tear\n  // them down on pointer-up, instead of re-binding on every render.\n  useEffect(() => {\n    if (!dragging) return\n    const onPointerMove = (e: PointerEvent) =>\n      moveVertexRef.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 = (index: number, dx: number, dy: number) => {\n    if (polygon === null || !onChange) return\n    const v = polygon.vertices[index]\n    const nx = pct(Number.parseFloat(v.x) + dx)\n    const ny = pct(Number.parseFloat(v.y) + dy)\n    const next = {\n      ...polygon,\n      vertices: polygon.vertices.map((vv, i) =>\n        i === index ? { x: nx, y: ny } : vv,\n      ),\n    }\n    onChange(formatClipPath({ ...state, shape: next }))\n  }\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        <div className=\"text-muted-foreground text-xs\">preview</div>\n        <div className=\"inline-flex overflow-hidden rounded-md border text-[10px]\">\n          {([\"clip-path\", \"shape-outside\"] as const).map((m) => (\n            <button\n              key={m}\n              type=\"button\"\n              aria-pressed={mode === m}\n              onClick={() => setMode(m)}\n              className={cn(\n                \"px-2 py-1 font-mono\",\n                mode === m\n                  ? \"bg-primary text-primary-foreground\"\n                  : \"bg-background text-muted-foreground\",\n              )}\n            >\n              {m}\n            </button>\n          ))}\n        </div>\n      </div>\n\n      <div\n        ref={stageRef}\n        data-testid=\"clip-preview-stage\"\n        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      >\n        <div\n          data-clip-target\n          className=\"absolute inset-0 bg-[linear-gradient(135deg,#0ea5e9,#8b5cf6,#ec4899)]\"\n          style={styleProp}\n          aria-hidden=\"true\"\n        />\n        {polygon !== null && onChange ? (\n          <svg\n            className=\"absolute inset-0 h-full w-full\"\n            aria-hidden=\"true\"\n            viewBox=\"0 0 100 100\"\n            preserveAspectRatio=\"none\"\n          >\n            <title>polygon outline</title>\n            <polygon\n              points={polygon.vertices\n                .map(\n                  (v) => `${Number.parseFloat(v.x)},${Number.parseFloat(v.y)}`,\n                )\n                .join(\" \")}\n              fill=\"none\"\n              stroke=\"white\"\n              strokeOpacity=\"0.7\"\n              strokeWidth=\"0.6\"\n              vectorEffect=\"non-scaling-stroke\"\n            />\n          </svg>\n        ) : null}\n        {polygon !== null && onChange\n          ? polygon.vertices.map((v, i) => (\n              <button\n                key={vertexIds[i]}\n                type=\"button\"\n                aria-label={`Vertex ${i + 1} at ${v.x} ${v.y}`}\n                onPointerDown={(e) => {\n                  dragRef.current = { index: i }\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(i, -big, 0)\n                  } else if (e.key === \"ArrowRight\") {\n                    e.preventDefault()\n                    nudge(i, big, 0)\n                  } else if (e.key === \"ArrowUp\") {\n                    e.preventDefault()\n                    nudge(i, 0, -big)\n                  } else if (e.key === \"ArrowDown\") {\n                    e.preventDefault()\n                    nudge(i, 0, big)\n                  }\n                }}\n                className=\"absolute h-3.5 w-3.5 -translate-x-1/2 -translate-y-1/2 cursor-grab rounded-full border-2 border-white bg-primary shadow focus:outline-none focus:ring-2 focus:ring-white active:cursor-grabbing\"\n                style={{\n                  left: `${Number.parseFloat(v.x)}%`,\n                  top: `${Number.parseFloat(v.y)}%`,\n                }}\n              />\n            ))\n          : null}\n      </div>\n\n      {polygon !== null && onChange ? (\n        <p className=\"text-center text-[10px] text-muted-foreground\">\n          drag a handle to move a vertex · arrow keys nudge (⇧ = 10%)\n        </p>\n      ) : null}\n\n      <CircleScrubs state={state} onChange={onChange} id={id} />\n    </div>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/clip-path-editor/preview/clip-path-preview.tsx"
    },
    {
      "path": "src/components/ui/clip-path-editor/primitives/geometry-box-select.tsx",
      "content": "import { cn } from \"@/lib/utils\"\nimport { GEOMETRY_BOXES } from \"../clip-path-editor.constants\"\nimport type { GeometryBox } from \"../clip-path-editor.types\"\n\nexport interface GeometryBoxSelectProps {\n  value: GeometryBox | undefined\n  onChange: (value: GeometryBox | undefined) => void\n  className?: string\n}\n\nexport function GeometryBoxSelect({\n  value,\n  onChange,\n  className,\n}: GeometryBoxSelectProps) {\n  return (\n    <select\n      aria-label=\"Geometry box\"\n      value={value ?? \"\"}\n      onChange={(e) =>\n        onChange(\n          e.target.value === \"\" ? undefined : (e.target.value as GeometryBox),\n        )\n      }\n      className={cn(\n        \"h-8 rounded border bg-background px-1.5 font-mono text-xs\",\n        className,\n      )}\n    >\n      <option value=\"\">no box</option>\n      {GEOMETRY_BOXES.map((box) => (\n        <option key={box} value={box}>\n          {box}\n        </option>\n      ))}\n    </select>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/clip-path-editor/primitives/geometry-box-select.tsx"
    },
    {
      "path": "src/components/ui/clip-path-editor/primitives/labeled-field.tsx",
      "content": "import type * as React from \"react\"\n\n/** A small uppercase label stacked above its control. */\nexport function LabeledField({\n  label,\n  children,\n}: {\n  label: string\n  children: React.ReactNode\n}) {\n  return (\n    <div className=\"flex flex-col gap-1 text-[10px] text-muted-foreground\">\n      <span className=\"font-mono uppercase tracking-wide\">{label}</span>\n      {children}\n    </div>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/clip-path-editor/primitives/labeled-field.tsx"
    },
    {
      "path": "src/components/ui/clip-path-editor/primitives/live-string.tsx",
      "content": "/** The read-only canonical string of the current clip-path value. */\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/clip-path-editor/primitives/live-string.tsx"
    },
    {
      "path": "src/components/ui/clip-path-editor/primitives/shape-select.tsx",
      "content": "import { cn } from \"@/lib/utils\"\nimport { SHAPES } from \"../clip-path-editor.constants\"\nimport type { BasicShapeName } from \"../clip-path-editor.types\"\n\nexport interface ShapeSelectProps {\n  value: BasicShapeName | undefined\n  onChange: (value: BasicShapeName | \"none\") => void\n  className?: string\n}\n\nexport function ShapeSelect({ value, onChange, className }: ShapeSelectProps) {\n  return (\n    <select\n      aria-label=\"Basic shape\"\n      value={value ?? \"none\"}\n      onChange={(e) => onChange(e.target.value as BasicShapeName | \"none\")}\n      className={cn(\n        \"h-8 rounded border bg-background px-1.5 font-mono text-xs\",\n        className,\n      )}\n    >\n      <option value=\"none\">none / box only</option>\n      {SHAPES.map((shape) => (\n        <option key={shape} value={shape}>\n          {shape}()\n        </option>\n      ))}\n    </select>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/clip-path-editor/primitives/shape-select.tsx"
    }
  ],
  "type": "registry:ui"
}