{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "transform-builder",
  "title": "Transform Builder",
  "description": "Ridiculously typed CSS transform editor — a space-separated function list with compile-time function-name dispatch (per-function arity and per-argument dimension typing: rotate wants an angle, translateX a length-%). Ships a tolerant runtime parser and a live 3D-preview card driven by the typed string.",
  "dependencies": [],
  "registryDependencies": [
    "ridiculous-type-kit",
    "button",
    "popover",
    "input",
    "label",
    "slider"
  ],
  "files": [
    {
      "path": "src/components/ui/transform-builder/index.ts",
      "content": "export type {\n  AddFunctionMenuProps,\n  ArgEditorProps,\n  TransformBuilderPanelProps,\n  TransformBuilderProps,\n  TransformFunctionRowProps,\n  TransformPreview3DProps,\n} from \"./transform-builder\"\nexport {\n  AddFunctionMenu,\n  ArgEditor,\n  TransformBuilder,\n  TransformBuilderPanel,\n  TransformFunctionRow,\n  TransformPreview3D,\n} from \"./transform-builder\"\nexport type {\n  ArgKind,\n  ArgSpec,\n} from \"./transform-builder.helpers\"\nexport {\n  argSpec,\n  defaultItem,\n  formatTransform,\n  itemToCss,\n  parseTransform,\n  transformFunctions,\n} from \"./transform-builder.helpers\"\nexport type {\n  Dimension,\n  FunctionCountOf,\n  FunctionsOf,\n  TransformFn,\n  TransformFunctionName,\n  TransformItem,\n  TransformLiteral,\n  TransformString,\n  TransformStringMap,\n} from \"./transform-builder.types\"\nexport { cssTransform } from \"./transform-builder.types\"\n",
      "type": "registry:ui",
      "target": "components/ui/transform-builder/index.ts"
    },
    {
      "path": "src/components/ui/transform-builder/transform-builder.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 {\n  AddFunctionMenu,\n  ArgEditor,\n  FunctionSelect,\n} from \"./transform-builder.arg-editor\"\nimport {\n  argSpec,\n  defaultItem,\n  formatTransform,\n  itemArgs,\n  itemFromArgs,\n  parseTransform,\n} from \"./transform-builder.helpers\"\nimport { TransformPreview3D } from \"./transform-builder.preview\"\nimport type {\n  TransformFunctionName,\n  TransformItem,\n  TransformString,\n} from \"./transform-builder.types\"\n\nexport type {\n  AddFunctionMenuProps,\n  ArgEditorProps,\n} from \"./transform-builder.arg-editor\"\n// Re-export the controls and the preview so the public surface of this\n// module is unchanged after the split (tests + index.ts import from here).\nexport {\n  AddFunctionMenu,\n  ArgEditor,\n} from \"./transform-builder.arg-editor\"\nexport type { TransformPreview3DProps } from \"./transform-builder.preview\"\nexport { TransformPreview3D } from \"./transform-builder.preview\"\n\n// ---------------------------------------------------------------------------\n// Shared props\n// ---------------------------------------------------------------------------\n\nexport interface TransformBuilderPanelProps {\n  value: TransformString | (string & {})\n  onChange: (value: TransformString) => void\n  className?: string\n  \"aria-label\"?: string\n}\n\nexport interface TransformBuilderProps extends TransformBuilderPanelProps {}\n\n// ---------------------------------------------------------------------------\n// TransformBuilder — popover-wrapped\n// ---------------------------------------------------------------------------\n\nexport function TransformBuilder(props: TransformBuilderProps) {\n  const {\n    value,\n    className,\n    \"aria-label\": ariaLabel = \"Edit a CSS transform\",\n  } = props\n  const items = parseTransform(String(value)) ?? []\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\">\n            {items.length} {items.length === 1 ? \"fn\" : \"fns\"}\n          </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        <TransformBuilderPanel {...props} />\n      </PopoverContent>\n    </Popover>\n  )\n}\n\n// ---------------------------------------------------------------------------\n// TransformBuilderPanel — inline\n// ---------------------------------------------------------------------------\n\nexport function TransformBuilderPanel({\n  value,\n  onChange,\n  className,\n  \"aria-label\": ariaLabel = \"CSS transform builder\",\n}: TransformBuilderPanelProps) {\n  const [items, setItems] = useState<TransformItem[]>(\n    () => parseTransform(String(value)) ?? [],\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 = parseTransform(String(value))\n    if (parsed !== null) setItems(parsed)\n  }, [value])\n\n  const commit = (next: TransformItem[]) => {\n    setItems(next)\n    const str = formatTransform(next)\n    lastEmittedRef.current = str\n    onChange(str as TransformString)\n  }\n\n  const updateAt = (index: number, item: TransformItem) => {\n    commit(items.map((it, i) => (i === index ? item : it)))\n  }\n  const removeAt = (index: number) => {\n    commit(items.filter((_, i) => i !== index))\n  }\n  const add = (fn: TransformFunctionName) => {\n    commit([...items, defaultItem(fn)])\n  }\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=\"space-y-2\">\n        {items.map((item, i) => (\n          <TransformFunctionRow\n            // biome-ignore lint/suspicious/noArrayIndexKey: rows are positional and reorderable only by add/remove\n            key={`${item.fn}-${i}`}\n            item={item}\n            onChange={(next) => updateAt(i, next)}\n            onRemove={() => removeAt(i)}\n          />\n        ))}\n      </div>\n      <AddFunctionMenu onAdd={add} />\n      <LiveString value={formatTransform(items)} />\n      <TransformPreview3D\n        value={formatTransform(items)}\n        onChange={(str) => {\n          const parsed = parseTransform(str)\n          if (parsed !== null) commit(parsed)\n        }}\n      />\n    </fieldset>\n  )\n}\n\n// ---------------------------------------------------------------------------\n// TransformFunctionRow (public)\n// ---------------------------------------------------------------------------\n\nexport interface TransformFunctionRowProps {\n  item: TransformItem\n  onChange: (item: TransformItem) => void\n  onRemove: () => void\n  className?: string\n}\n\nexport function TransformFunctionRow({\n  item,\n  onChange,\n  onRemove,\n  className,\n}: TransformFunctionRowProps) {\n  const spec = argSpec(item.fn)\n  const args = itemArgs(item)\n\n  const setArg = (slot: number, next: string) => {\n    const nextArgs = [...args]\n    nextArgs[slot] = next\n    onChange(itemFromArgs(item.fn, nextArgs))\n  }\n\n  const changeFn = (fn: TransformFunctionName) => {\n    onChange(defaultItem(fn))\n  }\n\n  // For optional-y functions, how many slots are currently shown.\n  const visibleSlots = Math.max(args.length, spec.min)\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      <FunctionSelect value={item.fn} onChange={changeFn} />\n      <div className=\"flex flex-1 flex-wrap items-center gap-1\">\n        {Array.from({ length: visibleSlots }, (_, slot) => (\n          <ArgEditor\n            // biome-ignore lint/suspicious/noArrayIndexKey: arg slots are fixed and positional per function; the row remounts when fn changes\n            key={`${item.fn}-arg-${slot}`}\n            fn={item.fn}\n            label={`${item.fn} ${spec.labels[slot] ?? `arg ${slot + 1}`}`}\n            kind={spec.kinds[slot] ?? spec.kinds[spec.kinds.length - 1]}\n            value={args[slot] ?? \"\"}\n            onChange={(next) => setArg(slot, next)}\n          />\n        ))}\n      </div>\n      <button\n        type=\"button\"\n        onClick={onRemove}\n        aria-label={`Remove ${item.fn}`}\n        className=\"rounded p-1 text-muted-foreground hover:bg-destructive/10 hover:text-destructive\"\n      >\n        <span aria-hidden=\"true\">×</span>\n      </button>\n    </div>\n  )\n}\n\n// ---------------------------------------------------------------------------\n// LiveString (internal)\n// ---------------------------------------------------------------------------\n\nfunction 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/transform-builder/transform-builder.tsx"
    },
    {
      "path": "src/components/ui/transform-builder/transform-builder.types.ts",
      "content": "// =====================================================================\n// transform-builder.types.ts\n//\n// The \"ridiculous\" tier: compile-time FUNCTION-NAME DISPATCH over a CSS\n// `transform` value (a space-separated list of transform functions).\n// Built entirely on `ridiculous-type-kit`. The strict validator\n// `TransformLiteral<S>` resolves to `S` when every function in the list\n// validates (its arity and each argument's DIMENSION), `never` otherwise.\n//\n//   \"translateX(10px) rotate(45deg) scale(1.5)\"  →  the literal\n//   \"rotate(10px)\"                               →  never (wants angle)\n//   \"translateX(45deg)\"                          →  never (wants length-%)\n//\n// Structure mirrors calc-editor.types.ts:\n//   kit imports → predicate aliases → arg folds → dispatch table →\n//   list validator → TransformLiteral + cssTransform → suggestion strings\n//   → utility types → internal discriminated-union state.\n//\n// This ESTABLISHES the function-list dispatch pattern Phase 3 (filter)\n// reuses: SplitBySpace → ParseFunction → ValidateFn signature table →\n// AllArgs* flat folds. Keep these pieces clean and self-contained.\n// =====================================================================\n\nimport type {\n  And,\n  IsAngle,\n  IsLength,\n  IsNumber,\n  IsPercentage,\n  Or,\n  ParseFunction,\n  SplitByComma,\n  SplitBySpace,\n  Trim,\n} from \"@/lib/ridiculous-type-kit\"\n\n// =====================================================================\n// 1. PER-DIMENSION PREDICATE ALIASES (Phase-3 reuse surface)\n// =====================================================================\n\n/** length OR percentage — translate axes. */\ntype IsLengthPct<S extends string> = Or<\n  IsLength<Trim<S>>,\n  IsPercentage<Trim<S>>\n>\n\n/** number OR percentage — scale factors. */\ntype IsNumberPct<S extends string> = Or<\n  IsNumber<Trim<S>>,\n  IsPercentage<Trim<S>>\n>\n\n// =====================================================================\n// 2. FLAT ARG FOLDS — every element must satisfy one predicate.\n//    Empty tuple → true (arity is validated separately by the caller).\n//    TS has no higher-kinded type params, so one fold per predicate.\n// =====================================================================\n\ntype AllLength<Args extends string[]> = Args extends [\n  infer H extends string,\n  ...infer T extends string[],\n]\n  ? IsLength<Trim<H>> extends true\n    ? AllLength<T>\n    : false\n  : true\n\ntype AllAngle<Args extends string[]> = Args extends [\n  infer H extends string,\n  ...infer T extends string[],\n]\n  ? IsAngle<Trim<H>> extends true\n    ? AllAngle<T>\n    : false\n  : true\n\ntype AllNumber<Args extends string[]> = Args extends [\n  infer H extends string,\n  ...infer T extends string[],\n]\n  ? IsNumber<Trim<H>> extends true\n    ? AllNumber<T>\n    : false\n  : true\n\ntype AllLengthPct<Args extends string[]> = Args 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\ntype AllNumberPct<Args extends string[]> = Args extends [\n  infer H extends string,\n  ...infer T extends string[],\n]\n  ? IsNumberPct<H> extends true\n    ? AllNumberPct<T>\n    : false\n  : true\n\n// =====================================================================\n// 3. DISPATCH TABLE — ValidateFn<Name, ArgStr> → true | false.\n//    The namesake. Arity is checked by tuple length; argument dimensions\n//    by the folds above (with positional special-cases for the 3d funcs).\n// =====================================================================\n\ntype ValidateFn<Name extends string, ArgStr extends string> =\n  SplitByComma<ArgStr> extends infer Args extends string[]\n    ? // --- translate family ---------------------------------------------\n      Name extends \"translateX\" | \"translateY\"\n      ? Args extends [string]\n        ? AllLengthPct<Args>\n        : false\n      : Name extends \"translateZ\" | \"perspective\"\n        ? Args extends [string]\n          ? AllLength<Args>\n          : false\n        : Name extends \"translate\"\n          ? Args extends [string] | [string, string]\n            ? AllLengthPct<Args>\n            : false\n          : Name extends \"translate3d\"\n            ? Args extends [\n                infer X extends string,\n                infer Y extends string,\n                infer Z extends string,\n              ]\n              ? And<IsLengthPct<X>, And<IsLengthPct<Y>, IsLength<Trim<Z>>>>\n              : false\n            : // --- rotate family --------------------------------------------\n              Name extends \"rotate\" | \"rotateX\" | \"rotateY\" | \"rotateZ\"\n              ? Args extends [string]\n                ? AllAngle<Args>\n                : false\n              : Name extends \"rotate3d\"\n                ? Args extends [\n                    infer A extends string,\n                    infer B extends string,\n                    infer C extends string,\n                    infer D extends string,\n                  ]\n                  ? And<\n                      IsNumber<Trim<A>>,\n                      And<\n                        IsNumber<Trim<B>>,\n                        And<IsNumber<Trim<C>>, IsAngle<Trim<D>>>\n                      >\n                    >\n                  : false\n                : // --- scale family -------------------------------------------\n                  Name extends \"scaleX\" | \"scaleY\" | \"scaleZ\"\n                  ? Args extends [string]\n                    ? AllNumberPct<Args>\n                    : false\n                  : Name extends \"scale\"\n                    ? Args extends [string] | [string, string]\n                      ? AllNumberPct<Args>\n                      : false\n                    : Name extends \"scale3d\"\n                      ? Args extends [string, string, string]\n                        ? AllNumberPct<Args>\n                        : false\n                      : // --- skew family ------------------------------------------\n                        Name extends \"skewX\" | \"skewY\"\n                        ? Args extends [string]\n                          ? AllAngle<Args>\n                          : false\n                        : Name extends \"skew\"\n                          ? Args extends [string] | [string, string]\n                            ? AllAngle<Args>\n                            : false\n                          : // --- matrix family --------------------------------------\n                            Name extends \"matrix\"\n                            ? Args extends [\n                                string,\n                                string,\n                                string,\n                                string,\n                                string,\n                                string,\n                              ]\n                              ? AllNumber<Args>\n                              : false\n                            : Name extends \"matrix3d\"\n                              ? Args[\"length\"] extends 16\n                                ? AllNumber<Args>\n                                : false\n                              : false // unknown function name\n    : false\n\n// Validate one space-separated token (`name(args)`), or false.\ntype ValidateToken<Token extends string> =\n  ParseFunction<Token> extends {\n    name: infer Name extends string\n    args: infer ArgStr extends string\n  }\n    ? ValidateFn<Name, ArgStr>\n    : false\n\n// Fold the space-separated function list; every token must validate.\ntype ValidateList<Tokens extends string[]> = Tokens extends [\n  infer H extends string,\n  ...infer T extends string[],\n]\n  ? ValidateToken<H> extends true\n    ? ValidateList<T>\n    : false\n  : true // empty list reached the end without a failure\n\n// =====================================================================\n// 4. STRICT VALIDATOR + CALL-SITE HELPER\n// =====================================================================\n\n/**\n * Strict literal validator. Resolves to `S` when `S` is a dimensionally-\n * and arity-valid CSS `transform` value (or the `none` keyword), `never`\n * otherwise. `calc()` / `var()` inside an argument resolve to `never` here\n * (undecidable at compile time) — use the casual / IntelliSense tier for\n * those; the runtime parser accepts them.\n *\n * @example\n * type A = TransformLiteral<\"translateX(10px) rotate(45deg)\"> // the literal\n * type B = TransformLiteral<\"rotate(10px)\">                   // never\n * type C = TransformLiteral<\"none\">                           // \"none\"\n */\nexport type TransformLiteral<S extends string> =\n  Trim<S> extends \"none\"\n    ? S\n    : Trim<S> extends \"\"\n      ? never\n      : SplitBySpace<Trim<S>> extends infer Tokens extends string[]\n        ? Tokens extends []\n          ? never\n          : ValidateList<Tokens> extends true\n            ? S\n            : never\n        : never\n\n/**\n * Call-site validator helper. Mirrors `cssCalc()` / `color()` / `easing()`.\n * An invalid transform becomes a type error at the argument.\n */\nexport const cssTransform = <S extends string>(\n  value: S & TransformLiteral<S>,\n): S => value\n\n// =====================================================================\n// 5. SUGGESTION STRINGS — IntelliSense + onChange return types\n// =====================================================================\n\n/** Every supported transform function name. */\nexport type TransformFunctionName =\n  | \"translate\"\n  | \"translateX\"\n  | \"translateY\"\n  | \"translateZ\"\n  | \"translate3d\"\n  | \"scale\"\n  | \"scaleX\"\n  | \"scaleY\"\n  | \"scaleZ\"\n  | \"scale3d\"\n  | \"rotate\"\n  | \"rotateX\"\n  | \"rotateY\"\n  | \"rotateZ\"\n  | \"rotate3d\"\n  | \"skew\"\n  | \"skewX\"\n  | \"skewY\"\n  | \"matrix\"\n  | \"matrix3d\"\n  | \"perspective\"\n\n/**\n * Suggestion union — \"this is a transform string\". A head-anchored\n * `` `${fn}(${string})` `` per function also matches multi-function lists\n * (the list starts with a function name and ends in `)`), plus `none`.\n */\nexport type TransformString = `${TransformFunctionName}(${string})` | \"none\"\n\n/** Function → output-string map. Backs the per-function suggestion shapes. */\nexport interface TransformStringMap {\n  translate: `translate(${string})`\n  translateX: `translateX(${string})`\n  translateY: `translateY(${string})`\n  translateZ: `translateZ(${string})`\n  translate3d: `translate3d(${string})`\n  scale: `scale(${string})`\n  scaleX: `scaleX(${string})`\n  scaleY: `scaleY(${string})`\n  scaleZ: `scaleZ(${string})`\n  scale3d: `scale3d(${string})`\n  rotate: `rotate(${string})`\n  rotateX: `rotateX(${string})`\n  rotateY: `rotateY(${string})`\n  rotateZ: `rotateZ(${string})`\n  rotate3d: `rotate3d(${string})`\n  skew: `skew(${string})`\n  skewX: `skewX(${string})`\n  skewY: `skewY(${string})`\n  matrix: `matrix(${string})`\n  matrix3d: `matrix3d(${string})`\n  perspective: `perspective(${string})`\n}\n\nexport type TransformFn = keyof TransformStringMap\n\n// =====================================================================\n// 6. UTILITY TYPES — operate on transform literals at the type level\n// =====================================================================\n\n/**\n * The ordered tuple of function names in a transform string.\n *\n * @example\n * type T = FunctionsOf<\"translateX(1px) rotate(45deg)\"> // [\"translateX\",\"rotate\"]\n * type N = FunctionsOf<\"none\">                          // []\n */\nexport type FunctionsOf<S extends string> =\n  Trim<S> extends \"none\" | \"\" ? [] : NamesOf<SplitBySpace<Trim<S>>>\n\ntype NamesOf<Tokens extends string[]> = Tokens extends [\n  infer H extends string,\n  ...infer T extends string[],\n]\n  ? ParseFunction<H> extends { name: infer Name extends string }\n    ? [Name, ...NamesOf<T>]\n    : NamesOf<T>\n  : []\n\n/**\n * The number of transform functions in the list.\n *\n * @example\n * type C = FunctionCountOf<\"translateX(1px) rotate(45deg)\"> // 2\n */\nexport type FunctionCountOf<S extends string> = FunctionsOf<S>[\"length\"]\n\n// =====================================================================\n// 7. INTERNAL STATE — discriminated union (exported)\n//\n// The editor's state is `TransformItem[]`. Each item is one function,\n// discriminated by `fn`. Exported for advanced use (custom serialization,\n// programmatic build). Argument values are kept as strings (they may carry\n// units), mirroring how the literal preserves the raw text.\n// =====================================================================\n\nexport type TransformItem =\n  // one length-percentage axis\n  | { fn: \"translateX\" | \"translateY\"; value: string }\n  // one length axis\n  | { fn: \"translateZ\" | \"perspective\"; value: string }\n  // two length-percentage axes (y optional in CSS; stored explicitly)\n  | { fn: \"translate\"; x: string; y?: string }\n  // three: x,y length-%, z length\n  | { fn: \"translate3d\"; x: string; y: string; z: string }\n  // one angle\n  | { fn: \"rotate\" | \"rotateX\" | \"rotateY\" | \"rotateZ\"; angle: string }\n  // axis vector + angle\n  | { fn: \"rotate3d\"; x: string; y: string; z: string; angle: string }\n  // one number-or-percentage factor\n  | { fn: \"scaleX\" | \"scaleY\" | \"scaleZ\"; value: string }\n  // two factors (y optional in CSS)\n  | { fn: \"scale\"; x: string; y?: string }\n  // three factors\n  | { fn: \"scale3d\"; x: string; y: string; z: string }\n  // one angle\n  | { fn: \"skewX\" | \"skewY\"; angle: string }\n  // two angles (y optional in CSS)\n  | { fn: \"skew\"; x: string; y?: string }\n  // 6-number 2D matrix\n  | { fn: \"matrix\"; values: string[] }\n  // 16-number 3D matrix\n  | { fn: \"matrix3d\"; values: string[] }\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/transform-builder/transform-builder.types.ts"
    },
    {
      "path": "src/components/ui/transform-builder/transform-builder.helpers.ts",
      "content": "// =====================================================================\n// transform-builder.helpers.ts\n//\n// Pure runtime parse / format / spec for CSS `transform` values. This is\n// the SUPERSET of the strict type tier: it tolerates calc()/var() inside\n// arguments (kept verbatim, opaque), validates arity, and drives the UI\n// from a single ARG_SPEC dispatch table.\n//\n// ARG_SPEC is the SINGLE SOURCE OF TRUTH. Every per-function behavior —\n// arity, slot kinds/labels, arg <-> item conversion, CSS serialization and\n// the seed default — lives in one row. The five operations below\n// (`buildItem`, `defaultItem`, `itemToCss`, `itemArgs`, `itemFromArgs`) are\n// THIN readers of that table; adding a function = adding one row.\n// =====================================================================\n\nimport type {\n  TransformFunctionName,\n  TransformItem,\n} from \"./transform-builder.types\"\n\n// ---------------------------------------------------------------------------\n// ARG_SPEC — the runtime dispatch table (single source of truth)\n// ---------------------------------------------------------------------------\n\n/** Dimension family an argument slot accepts. Mirrors the type predicates. */\nexport type ArgKind =\n  | \"length-percentage\"\n  | \"length\"\n  | \"angle\"\n  | \"number\"\n  | \"number-percentage\"\n\nexport interface ArgSpec {\n  /** Minimum argument count. */\n  min: number\n  /** Maximum argument count. */\n  max: number\n  /** Per-slot dimension kinds (length === max for fixed-arity functions). */\n  kinds: ArgKind[]\n  /** Axis / slot labels for the UI. */\n  labels: string[]\n  /** Seed item for a freshly-added row. */\n  default: TransformItem\n  /** Pull the per-slot argument strings out of an item, for editing. */\n  toArgs: (item: TransformItem) => string[]\n  /** Rebuild an item from edited argument strings. */\n  fromArgs: (args: string[]) => TransformItem\n  /** Serialize an item to its CSS function string. */\n  toCss: (item: TransformItem) => string\n}\n\nconst LP: ArgKind = \"length-percentage\"\nconst LEN: ArgKind = \"length\"\nconst ANG: ArgKind = \"angle\"\nconst NUM: ArgKind = \"number\"\nconst NP: ArgKind = \"number-percentage\"\n\n// ---------------------------------------------------------------------------\n// Shape families — one set of {toArgs, fromArgs, toCss} per item shape.\n//\n// Each TransformItem variant is one of six shapes. Encoding the conversions\n// per shape (rather than per function) is what collapses the five duplicated\n// `switch(fn)` ladders into table lookups. The casts narrow the union to the\n// shape the row is known to hold — `argSpec(item.fn)` is only ever called\n// with an item whose `fn` selects a row of the matching shape.\n// ---------------------------------------------------------------------------\n\ninterface Shape {\n  toArgs: (item: TransformItem) => string[]\n  fromArgs: (fn: TransformFunctionName, args: string[]) => TransformItem\n  toCss: (item: TransformItem) => string\n}\n\n/** One `value` slot: translateX/Y/Z, perspective, scaleX/Y/Z. */\nconst valueShape: Shape = {\n  toArgs: (item) => [(item as { value: string }).value],\n  fromArgs: (fn, args) => ({ fn, value: args[0] }) as TransformItem,\n  toCss: (item) => `${item.fn}(${(item as { value: string }).value})`,\n}\n\n/** One `angle` slot: rotate/X/Y/Z, skewX/Y. */\nconst angleShape: Shape = {\n  toArgs: (item) => [(item as { angle: string }).angle],\n  fromArgs: (fn, args) => ({ fn, angle: args[0] }) as TransformItem,\n  toCss: (item) => `${item.fn}(${(item as { angle: string }).angle})`,\n}\n\n/** One required `x`, optional `y`: translate, scale, skew. */\nconst xyOptionalShape: Shape = {\n  toArgs: (item) => {\n    const it = item as { x: string; y?: string }\n    return it.y === undefined ? [it.x] : [it.x, it.y]\n  },\n  fromArgs: (fn, args) =>\n    (args.length > 1 && args[1] !== undefined\n      ? { fn, x: args[0], y: args[1] }\n      : { fn, x: args[0] }) as TransformItem,\n  toCss: (item) => {\n    const it = item as { x: string; y?: string }\n    return it.y === undefined\n      ? `${item.fn}(${it.x})`\n      : `${item.fn}(${it.x}, ${it.y})`\n  },\n}\n\n/** Three slots x,y,z: translate3d, scale3d. */\nconst xyzShape: Shape = {\n  toArgs: (item) => {\n    const it = item as { x: string; y: string; z: string }\n    return [it.x, it.y, it.z]\n  },\n  fromArgs: (fn, args) =>\n    ({ fn, x: args[0], y: args[1], z: args[2] }) as TransformItem,\n  toCss: (item) => {\n    const it = item as { x: string; y: string; z: string }\n    return `${item.fn}(${it.x}, ${it.y}, ${it.z})`\n  },\n}\n\n/** Axis vector + angle: rotate3d. */\nconst xyzAngleShape: Shape = {\n  toArgs: (item) => {\n    const it = item as { x: string; y: string; z: string; angle: string }\n    return [it.x, it.y, it.z, it.angle]\n  },\n  fromArgs: (fn, args) =>\n    ({\n      fn,\n      x: args[0],\n      y: args[1],\n      z: args[2],\n      angle: args[3],\n    }) as TransformItem,\n  toCss: (item) => {\n    const it = item as { x: string; y: string; z: string; angle: string }\n    return `${item.fn}(${it.x}, ${it.y}, ${it.z}, ${it.angle})`\n  },\n}\n\n/** N-tuple of numbers: matrix, matrix3d. */\nconst valuesShape: Shape = {\n  toArgs: (item) => (item as { values: string[] }).values,\n  fromArgs: (fn, args) => ({ fn, values: args }) as TransformItem,\n  toCss: (item) =>\n    `${item.fn}(${(item as { values: string[] }).values.join(\", \")})`,\n}\n\n/**\n * Build one ARG_SPEC row. Binds a shape's conversions to a concrete `fn`\n * (so `fromArgs` needs no argument) and carries the row's arity + UI metadata.\n */\nfunction row(\n  fn: TransformFunctionName,\n  shape: Shape,\n  spec: {\n    min: number\n    max: number\n    kinds: ArgKind[]\n    labels: string[]\n    default: TransformItem\n  },\n): ArgSpec {\n  return {\n    min: spec.min,\n    max: spec.max,\n    kinds: spec.kinds,\n    labels: spec.labels,\n    default: spec.default,\n    toArgs: shape.toArgs,\n    fromArgs: (args) => shape.fromArgs(fn, args),\n    toCss: shape.toCss,\n  }\n}\n\nconst M3D_DEFAULT: string[] = [\n  \"1\",\n  \"0\",\n  \"0\",\n  \"0\",\n  \"0\",\n  \"1\",\n  \"0\",\n  \"0\",\n  \"0\",\n  \"0\",\n  \"1\",\n  \"0\",\n  \"0\",\n  \"0\",\n  \"0\",\n  \"1\",\n]\n\nconst ARG_SPEC: Record<TransformFunctionName, ArgSpec> = {\n  translate: row(\"translate\", xyOptionalShape, {\n    min: 1,\n    max: 2,\n    kinds: [LP, LP],\n    labels: [\"x\", \"y\"],\n    default: { fn: \"translate\", x: \"0px\", y: \"0px\" },\n  }),\n  translateX: row(\"translateX\", valueShape, {\n    min: 1,\n    max: 1,\n    kinds: [LP],\n    labels: [\"x\"],\n    default: { fn: \"translateX\", value: \"0px\" },\n  }),\n  translateY: row(\"translateY\", valueShape, {\n    min: 1,\n    max: 1,\n    kinds: [LP],\n    labels: [\"y\"],\n    default: { fn: \"translateY\", value: \"0px\" },\n  }),\n  translateZ: row(\"translateZ\", valueShape, {\n    min: 1,\n    max: 1,\n    kinds: [LEN],\n    labels: [\"z\"],\n    default: { fn: \"translateZ\", value: \"0px\" },\n  }),\n  translate3d: row(\"translate3d\", xyzShape, {\n    min: 3,\n    max: 3,\n    kinds: [LP, LP, LEN],\n    labels: [\"x\", \"y\", \"z\"],\n    default: { fn: \"translate3d\", x: \"0px\", y: \"0px\", z: \"0px\" },\n  }),\n  scale: row(\"scale\", xyOptionalShape, {\n    min: 1,\n    max: 2,\n    kinds: [NP, NP],\n    labels: [\"x\", \"y\"],\n    default: { fn: \"scale\", x: \"1\" },\n  }),\n  scaleX: row(\"scaleX\", valueShape, {\n    min: 1,\n    max: 1,\n    kinds: [NP],\n    labels: [\"x\"],\n    default: { fn: \"scaleX\", value: \"1\" },\n  }),\n  scaleY: row(\"scaleY\", valueShape, {\n    min: 1,\n    max: 1,\n    kinds: [NP],\n    labels: [\"y\"],\n    default: { fn: \"scaleY\", value: \"1\" },\n  }),\n  scaleZ: row(\"scaleZ\", valueShape, {\n    min: 1,\n    max: 1,\n    kinds: [NP],\n    labels: [\"z\"],\n    default: { fn: \"scaleZ\", value: \"1\" },\n  }),\n  scale3d: row(\"scale3d\", xyzShape, {\n    min: 3,\n    max: 3,\n    kinds: [NP, NP, NP],\n    labels: [\"x\", \"y\", \"z\"],\n    default: { fn: \"scale3d\", x: \"1\", y: \"1\", z: \"1\" },\n  }),\n  rotate: row(\"rotate\", angleShape, {\n    min: 1,\n    max: 1,\n    kinds: [ANG],\n    labels: [\"angle\"],\n    default: { fn: \"rotate\", angle: \"0deg\" },\n  }),\n  rotateX: row(\"rotateX\", angleShape, {\n    min: 1,\n    max: 1,\n    kinds: [ANG],\n    labels: [\"angle\"],\n    default: { fn: \"rotateX\", angle: \"0deg\" },\n  }),\n  rotateY: row(\"rotateY\", angleShape, {\n    min: 1,\n    max: 1,\n    kinds: [ANG],\n    labels: [\"angle\"],\n    default: { fn: \"rotateY\", angle: \"0deg\" },\n  }),\n  rotateZ: row(\"rotateZ\", angleShape, {\n    min: 1,\n    max: 1,\n    kinds: [ANG],\n    labels: [\"angle\"],\n    default: { fn: \"rotateZ\", angle: \"0deg\" },\n  }),\n  rotate3d: row(\"rotate3d\", xyzAngleShape, {\n    min: 4,\n    max: 4,\n    kinds: [NUM, NUM, NUM, ANG],\n    labels: [\"x\", \"y\", \"z\", \"angle\"],\n    default: { fn: \"rotate3d\", x: \"1\", y: \"1\", z: \"1\", angle: \"0deg\" },\n  }),\n  skew: row(\"skew\", xyOptionalShape, {\n    min: 1,\n    max: 2,\n    kinds: [ANG, ANG],\n    labels: [\"x\", \"y\"],\n    default: { fn: \"skew\", x: \"0deg\", y: \"0deg\" },\n  }),\n  skewX: row(\"skewX\", angleShape, {\n    min: 1,\n    max: 1,\n    kinds: [ANG],\n    labels: [\"x\"],\n    default: { fn: \"skewX\", angle: \"0deg\" },\n  }),\n  skewY: row(\"skewY\", angleShape, {\n    min: 1,\n    max: 1,\n    kinds: [ANG],\n    labels: [\"y\"],\n    default: { fn: \"skewY\", angle: \"0deg\" },\n  }),\n  matrix: row(\"matrix\", valuesShape, {\n    min: 6,\n    max: 6,\n    kinds: [NUM, NUM, NUM, NUM, NUM, NUM],\n    labels: [\"a\", \"b\", \"c\", \"d\", \"e\", \"f\"],\n    default: { fn: \"matrix\", values: [\"1\", \"0\", \"0\", \"1\", \"0\", \"0\"] },\n  }),\n  matrix3d: row(\"matrix3d\", valuesShape, {\n    min: 16,\n    max: 16,\n    kinds: Array.from({ length: 16 }, () => NUM),\n    labels: Array.from({ length: 16 }, (_, i) => `m${i + 1}`),\n    default: { fn: \"matrix3d\", values: M3D_DEFAULT },\n  }),\n  perspective: row(\"perspective\", valueShape, {\n    min: 1,\n    max: 1,\n    kinds: [LEN],\n    labels: [\"depth\"],\n    default: { fn: \"perspective\", value: \"800px\" },\n  }),\n}\n\nconst FUNCTION_NAMES = new Set<string>(Object.keys(ARG_SPEC))\n\nfunction isFunctionName(name: string): name is TransformFunctionName {\n  return FUNCTION_NAMES.has(name)\n}\n\n/** The argument spec for a function — drives both parsing and the UI. */\nexport function argSpec(fn: TransformFunctionName): ArgSpec {\n  return ARG_SPEC[fn]\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 a transform list into function tokens, dropping empty runs. */\nfunction splitFunctions(src: string): string[] {\n  return splitTopLevel(src, \" \")\n    .map((s) => s.trim())\n    .filter((s) => s.length > 0)\n}\n\n/** Split a function's argument string into trimmed args (drops empty). */\nfunction splitArgs(argStr: string): string[] {\n  const trimmed = argStr.trim()\n  if (trimmed === \"\") return []\n  return splitTopLevel(trimmed, \",\").map((s) => s.trim())\n}\n\nconst CALL_RE = /^([a-zA-Z][a-zA-Z0-9]*)\\((.*)\\)$/s\n\n// ---------------------------------------------------------------------------\n// parseTransform — string → TransformItem[] | null\n// ---------------------------------------------------------------------------\n\nfunction buildItem(\n  fn: TransformFunctionName,\n  args: string[],\n): TransformItem | null {\n  const spec = ARG_SPEC[fn]\n  if (args.length < spec.min || args.length > spec.max) return null\n  return spec.fromArgs(args)\n}\n\n/**\n * Parse a CSS `transform` value into typed items, or `null` on any syntax,\n * unknown-function, or arity error. `none` / empty → `[]`.\n */\nexport function parseTransform(src: string): TransformItem[] | null {\n  const trimmed = src.trim()\n  if (trimmed === \"\" || trimmed === \"none\") return []\n\n  const tokens = splitFunctions(trimmed)\n  if (tokens.length === 0) return []\n\n  const items: TransformItem[] = []\n  for (const token of tokens) {\n    const m = token.match(CALL_RE)\n    if (!m) return null\n    const name = m[1]\n    if (!isFunctionName(name)) return null\n    const args = splitArgs(m[2])\n    const item = buildItem(name, args)\n    if (item === null) return null\n    items.push(item)\n  }\n  return items\n}\n\n/** Runtime mirror of `FunctionsOf` — list the function names in order. */\nexport function transformFunctions(src: string): string[] {\n  const items = parseTransform(src)\n  if (items === null) return []\n  return items.map((it) => it.fn)\n}\n\n// ---------------------------------------------------------------------------\n// defaultItem — seed a fresh row\n// ---------------------------------------------------------------------------\n\n/** A sensible default item for a freshly-added function row. */\nexport function defaultItem(fn: TransformFunctionName): TransformItem {\n  return ARG_SPEC[fn].default\n}\n\n// ---------------------------------------------------------------------------\n// item ↔ args — per-slot editing surface for the UI\n// ---------------------------------------------------------------------------\n\n/** Pull the per-slot argument strings out of an item, for editing. */\nexport function itemArgs(item: TransformItem): string[] {\n  return ARG_SPEC[item.fn].toArgs(item)\n}\n\n/** Rebuild an item from edited argument strings. */\nexport function itemFromArgs(\n  fn: TransformFunctionName,\n  args: string[],\n): TransformItem {\n  return ARG_SPEC[fn].fromArgs(args)\n}\n\n// ---------------------------------------------------------------------------\n// itemToCss / formatTransform — canonical serialization\n// ---------------------------------------------------------------------------\n\n/** Serialize one item to its CSS function string. */\nexport function itemToCss(item: TransformItem): string {\n  return ARG_SPEC[item.fn].toCss(item)\n}\n\n/** Canonical re-serialization of a transform list. Empty → `none`. */\nexport function formatTransform(items: TransformItem[]): string {\n  if (items.length === 0) return \"none\"\n  return items.map(itemToCss).join(\" \")\n}\n",
      "type": "registry:ui",
      "target": "components/ui/transform-builder/transform-builder.helpers.ts"
    },
    {
      "path": "src/components/ui/transform-builder/transform-builder.arg-editor.tsx",
      "content": "\"use client\"\n\nimport { Input } from \"@/components/ui/input\"\nimport { cn } from \"@/lib/utils\"\nimport {\n  ANGLE_UNITS,\n  FUNCTION_GROUPS,\n  LENGTH_UNITS,\n  NUMBER_UNIT_RE,\n  UNITLESS,\n} from \"./transform-builder.constants\"\nimport type { ArgKind } from \"./transform-builder.helpers\"\nimport type { TransformFunctionName } from \"./transform-builder.types\"\n\n// ---------------------------------------------------------------------------\n// ArgEditor (public) — a single argument slot: number + unit, or raw text\n// ---------------------------------------------------------------------------\n\nexport interface ArgEditorProps {\n  fn: TransformFunctionName\n  label: string\n  kind: ArgKind\n  value: string\n  onChange: (value: string) => void\n  className?: string\n}\n\nexport function ArgEditor({\n  label,\n  kind,\n  value,\n  onChange,\n  className,\n}: ArgEditorProps) {\n  const unitless = UNITLESS.has(kind)\n  const units = kind === \"angle\" ? ANGLE_UNITS : LENGTH_UNITS\n\n  // Split a value like \"10px\" into number + unit for the dual control.\n  // Requires at least one digit (so \"\", \"-\", and a bare unit like \"px\" stay\n  // opaque rather than emitting \"-px\"/\"0px\"); accepts an exponent (\"1e3px\").\n  const m = value.match(NUMBER_UNIT_RE)\n  const numPart = m ? m[1] : value\n  const unitPart = m ? m[2] : \"\"\n  const opaque = m === null // \"\", lone sign, calc()/var() etc — show raw\n\n  if (opaque || unitless) {\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-[88px] 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={value}\n        spellCheck={false}\n        autoComplete=\"off\"\n        onChange={(e) => onChange(e.target.value)}\n        className=\"h-8 w-[72px] rounded-r-none border-r-0 font-mono text-xs\"\n      />\n      <select\n        aria-label={`${label} unit`}\n        value={unitPart || units[0]}\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        {units.map((u) => (\n          <option key={u} value={u}>\n            {u}\n          </option>\n        ))}\n      </select>\n    </span>\n  )\n}\n\n// ---------------------------------------------------------------------------\n// FunctionSelect (internal) — pick the function for an existing row\n// ---------------------------------------------------------------------------\n\nexport function FunctionSelect({\n  value,\n  onChange,\n}: {\n  value: TransformFunctionName\n  onChange: (fn: TransformFunctionName) => void\n}) {\n  return (\n    <select\n      aria-label=\"Transform function\"\n      value={value}\n      onChange={(e) => onChange(e.target.value as TransformFunctionName)}\n      className=\"h-8 rounded border bg-background px-1.5 font-mono text-xs\"\n    >\n      {FUNCTION_GROUPS.map((group) => (\n        <optgroup key={group.label} label={group.label}>\n          {group.fns.map((fn) => (\n            <option key={fn} value={fn}>\n              {fn}\n            </option>\n          ))}\n        </optgroup>\n      ))}\n    </select>\n  )\n}\n\n// ---------------------------------------------------------------------------\n// AddFunctionMenu (public) — append a new function row\n// ---------------------------------------------------------------------------\n\nexport interface AddFunctionMenuProps {\n  onAdd: (fn: TransformFunctionName) => void\n  className?: string\n}\n\nexport function AddFunctionMenu({ onAdd, className }: AddFunctionMenuProps) {\n  return (\n    <select\n      aria-label=\"Add a transform function\"\n      value=\"\"\n      onChange={(e) => {\n        const fn = e.target.value\n        if (fn) onAdd(fn as TransformFunctionName)\n        e.target.value = \"\"\n      }}\n      className={cn(\n        \"h-9 w-full rounded-md border border-dashed bg-background px-2 font-mono text-muted-foreground text-xs\",\n        className,\n      )}\n    >\n      <option value=\"\">+ add function…</option>\n      {FUNCTION_GROUPS.map((group) => (\n        <optgroup key={group.label} label={group.label}>\n          {group.fns.map((fn) => (\n            <option key={fn} value={fn}>\n              {fn}\n            </option>\n          ))}\n        </optgroup>\n      ))}\n    </select>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/transform-builder/transform-builder.arg-editor.tsx"
    },
    {
      "path": "src/components/ui/transform-builder/transform-builder.constants.ts",
      "content": "// =====================================================================\n// transform-builder.constants.ts\n//\n// Shared, render-free constants and small types for the transform builder\n// UI: the function option-groups, the unit lists, the number/unit split\n// regex, the unitless-kind set, and the 3D-preview scrub descriptors.\n// Kept in a non-component module so the .tsx files stay component-only\n// (avoids react-refresh/only-export-components churn).\n// =====================================================================\n\nimport type { ArgKind } from \"./transform-builder.helpers\"\nimport type { TransformFunctionName } from \"./transform-builder.types\"\n\n// ---------------------------------------------------------------------------\n// Function groups (for the <select> option-groups)\n// ---------------------------------------------------------------------------\n\nexport const FUNCTION_GROUPS: ReadonlyArray<{\n  label: string\n  fns: readonly TransformFunctionName[]\n}> = [\n  {\n    label: \"translate\",\n    fns: [\"translate\", \"translateX\", \"translateY\", \"translateZ\", \"translate3d\"],\n  },\n  { label: \"scale\", fns: [\"scale\", \"scaleX\", \"scaleY\", \"scaleZ\", \"scale3d\"] },\n  {\n    label: \"rotate\",\n    fns: [\"rotate\", \"rotateX\", \"rotateY\", \"rotateZ\", \"rotate3d\"],\n  },\n  { label: \"skew\", fns: [\"skew\", \"skewX\", \"skewY\"] },\n  { label: \"matrix\", fns: [\"matrix\", \"matrix3d\"] },\n  { label: \"perspective\", fns: [\"perspective\"] },\n]\n\n// ---------------------------------------------------------------------------\n// Units + the number/unit split\n// ---------------------------------------------------------------------------\n\nexport const LENGTH_UNITS = [\"px\", \"rem\", \"em\", \"%\", \"vw\", \"vh\"] as const\nexport const ANGLE_UNITS = [\"deg\", \"grad\", \"rad\", \"turn\"] as const\n\n/**\n * Split a dimension like `10px` / `-1.5e3rem` / `50%` into [number, unit].\n * The number requires at least one digit (rejecting `\"\"`, a lone `-`, and a\n * bare unit) and admits an optional sign, fraction, and exponent; the unit is\n * letters or `%`. Non-matches are treated as opaque (calc()/var(), raw text).\n */\nexport const NUMBER_UNIT_RE =\n  /^([+-]?(?:\\d+\\.?\\d*|\\.\\d+)(?:e[+-]?\\d+)?)([a-z%]*)$/i\n\n/** Kinds with no unit picker — the value is edited as raw text. */\nexport const UNITLESS: ReadonlySet<ArgKind> = new Set<ArgKind>([\n  \"number\",\n  \"number-percentage\",\n])\n\n// ---------------------------------------------------------------------------\n// 3D-preview scrubbers\n// ---------------------------------------------------------------------------\n\nexport interface Scrub {\n  fn: TransformFunctionName\n  label: string\n  min: number\n  max: number\n  step: number\n  unit: string\n}\n\nexport const SCRUBS: readonly Scrub[] = [\n  {\n    fn: \"translateX\",\n    label: \"translateX\",\n    min: -100,\n    max: 100,\n    step: 1,\n    unit: \"px\",\n  },\n  {\n    fn: \"translateY\",\n    label: \"translateY\",\n    min: -100,\n    max: 100,\n    step: 1,\n    unit: \"px\",\n  },\n  { fn: \"rotate\", label: \"rotate\", min: -180, max: 180, step: 1, unit: \"deg\" },\n  { fn: \"scale\", label: \"scale\", min: 0, max: 2, step: 0.05, unit: \"\" },\n  { fn: \"skewX\", label: \"skewX\", min: -45, max: 45, step: 1, unit: \"deg\" },\n]\n",
      "type": "registry:ui",
      "target": "components/ui/transform-builder/transform-builder.constants.ts"
    },
    {
      "path": "src/components/ui/transform-builder/transform-builder.preview.tsx",
      "content": "\"use client\"\n\nimport { useId } from \"react\"\nimport { cn } from \"@/lib/utils\"\nimport { SCRUBS, type Scrub } from \"./transform-builder.constants\"\nimport {\n  formatTransform,\n  itemArgs,\n  itemFromArgs,\n  parseTransform,\n} from \"./transform-builder.helpers\"\nimport type { TransformItem } from \"./transform-builder.types\"\n\n// ---------------------------------------------------------------------------\n// TransformPreview3D (public) — the showcase 3D scene + scrubbers\n// ---------------------------------------------------------------------------\n\nexport interface TransformPreview3DProps {\n  value: string\n  onChange?: (value: string) => void\n  className?: string\n}\n\n/** Read the current scalar for a scrub fn from a parsed list (or a default). */\nfunction scrubValue(items: TransformItem[], scrub: Scrub): number {\n  const item = items.find((it) => it.fn === scrub.fn)\n  if (!item) return scrub.fn === \"scale\" ? 1 : 0\n  const raw =\n    \"angle\" in item\n      ? item.angle\n      : \"value\" in item\n        ? item.value\n        : \"x\" in item\n          ? item.x\n          : \"0\"\n  const n = Number.parseFloat(raw)\n  return Number.isNaN(n) ? (scrub.fn === \"scale\" ? 1 : 0) : n\n}\n\n/**\n * Merge a scrub change into the list. A scrub retunes only the primary\n * (slot-0) scalar of its function: when that function already exists we keep\n * its remaining args (e.g. the optional `y` of `scale(x, y)`) and rewrite just\n * slot 0; otherwise we append a fresh single-arg item.\n */\nfunction applyScrub(\n  items: TransformItem[],\n  scrub: Scrub,\n  n: number,\n): TransformItem[] {\n  const slot0 = `${n}${scrub.unit}`\n  const idx = items.findIndex((it) => it.fn === scrub.fn)\n  if (idx === -1) {\n    return [...items, itemFromArgs(scrub.fn, [slot0])]\n  }\n  return items.map((it, i) => {\n    if (i !== idx) return it\n    // Copy first: itemArgs can return the live array (valuesShape returns\n    // `item.values` by reference), so mutating in place would corrupt state.\n    const args = [...itemArgs(it)]\n    args[0] = slot0\n    return itemFromArgs(scrub.fn, args)\n  })\n}\n\nexport function TransformPreview3D({\n  value,\n  onChange,\n  className,\n}: TransformPreview3DProps) {\n  const id = useId()\n  const items = parseTransform(value) ?? []\n\n  return (\n    <div className={cn(\"space-y-3 rounded-lg border p-3\", className)}>\n      <div className=\"text-muted-foreground text-xs\">3D preview</div>\n      <div\n        className=\"flex h-40 items-center justify-center rounded-md bg-muted/30\"\n        style={{ perspective: \"600px\" }}\n      >\n        <div\n          data-transform-card\n          className=\"flex h-20 w-28 items-center justify-center rounded-lg bg-gradient-to-br from-primary to-primary/60 font-mono text-[10px] text-primary-foreground shadow-lg\"\n          style={{ transform: value === \"none\" ? undefined : value }}\n          aria-hidden=\"true\"\n        >\n          transform\n        </div>\n      </div>\n      {onChange ? (\n        <div className=\"space-y-1.5\">\n          {SCRUBS.map((scrub) => {\n            const current = scrubValue(items, scrub)\n            return (\n              <label\n                key={scrub.fn}\n                htmlFor={`${id}-${scrub.fn}`}\n                className=\"flex items-center gap-2 text-xs\"\n              >\n                <span className=\"w-20 font-mono text-muted-foreground\">\n                  {scrub.label}\n                </span>\n                <input\n                  id={`${id}-${scrub.fn}`}\n                  type=\"range\"\n                  aria-label={scrub.label}\n                  min={scrub.min}\n                  max={scrub.max}\n                  step={scrub.step}\n                  value={current}\n                  onChange={(e) =>\n                    onChange(\n                      formatTransform(\n                        applyScrub(items, scrub, Number(e.target.value)),\n                      ),\n                    )\n                  }\n                  className=\"flex-1\"\n                />\n                <span className=\"w-14 text-right font-mono text-muted-foreground\">\n                  {current}\n                  {scrub.unit}\n                </span>\n              </label>\n            )\n          })}\n        </div>\n      ) : null}\n    </div>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/transform-builder/transform-builder.preview.tsx"
    }
  ],
  "type": "registry:ui"
}