{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "anchor-position-editor",
  "title": "Anchor Position Editor",
  "description": "Ridiculously typed editor for CSS anchor positioning (mode prop). The strict tier enforces the position-area cross-axis rule — a keyword pair must sit on two different axes of the same coordinate system. The hero is a clickable 3×3 placement grid with a live snap preview.",
  "dependencies": [],
  "registryDependencies": [
    "ridiculous-type-kit",
    "unit-input",
    "button",
    "popover",
    "input",
    "label"
  ],
  "files": [
    {
      "path": "src/components/ui/anchor-position-editor/index.ts",
      "content": "export type {\n  AnchorExprFieldsProps,\n  AnchorPositionEditorPanelProps,\n  AnchorPositionEditorProps,\n  AnchorPreviewProps,\n  MiniSelectProps,\n  PositionAreaGridProps,\n  TryFallbackChainProps,\n} from \"./anchor-position-editor\"\nexport {\n  AnchorExprFields,\n  AnchorPositionEditor,\n  AnchorPositionEditorPanel,\n  AnchorPreview,\n  LiveString,\n  MiniSelect,\n  PositionAreaGrid,\n  TryFallbackChain,\n} from \"./anchor-position-editor\"\nexport {\n  anchorSides,\n  anchorSizes,\n  areCompatible,\n  axisOf,\n  cellToKeywords,\n  defaultFor,\n  formatAnchor,\n  formatPositionArea,\n  formatPositionTry,\n  keywordsToCell,\n  parseAnchor,\n  parsePositionArea,\n  parsePositionTry,\n  positionAreaKeywords,\n  tryTactics,\n} from \"./anchor-position-editor.helpers\"\nexport type {\n  AnchorExpr,\n  AnchorLiteral,\n  AnchorPositionMode,\n  AnchorPositionString,\n  AnchorSideKeyword,\n  AnchorSizeKeyword,\n  AnchorString,\n  AnchorStringMap,\n  AxisOf,\n  Compatible,\n  KeywordsOf,\n  PaKeyword,\n  PositionAreaLiteral,\n  PositionAreaState,\n  PositionAreaString,\n  PositionAxis,\n  PositionTryLiteral,\n  PositionTryString,\n  TryFallback,\n  TryTactic,\n} from \"./anchor-position-editor.types\"\nexport {\n  cssAnchor,\n  cssPositionArea,\n  cssPositionTry,\n} from \"./anchor-position-editor.types\"\n",
      "type": "registry:ui",
      "target": "components/ui/anchor-position-editor/index.ts"
    },
    {
      "path": "src/components/ui/anchor-position-editor/anchor-position-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 { AnchorExprFields } from \"./anchor-expr-fields\"\nimport {\n  formatAnchor,\n  formatPositionArea,\n  formatPositionTry,\n  keywordsToCell,\n  parseAnchor,\n  parsePositionArea,\n  parsePositionTry,\n} from \"./anchor-position-editor.helpers\"\nimport type {\n  AnchorExpr,\n  AnchorPositionMode,\n  AnchorPositionString,\n  PositionAreaState,\n  TryFallback,\n} from \"./anchor-position-editor.types\"\nimport { AnchorPreview } from \"./anchor-preview\"\nimport { PositionAreaGrid, stateToKeywords } from \"./position-area-grid\"\nimport { TryFallbackChain } from \"./try-fallback-chain\"\n\n// Re-export the public sub-components + their prop types so consumers (and the\n// barrel) can import them from `./anchor-position-editor`.\nexport type { AnchorExprFieldsProps } from \"./anchor-expr-fields\"\nexport { AnchorExprFields } from \"./anchor-expr-fields\"\nexport type { AnchorPreviewProps } from \"./anchor-preview\"\nexport { AnchorPreview } from \"./anchor-preview\"\nexport type { MiniSelectProps } from \"./mini-select\"\nexport { MiniSelect } from \"./mini-select\"\nexport type { PositionAreaGridProps } from \"./position-area-grid\"\nexport { PositionAreaGrid } from \"./position-area-grid\"\nexport type { TryFallbackChainProps } from \"./try-fallback-chain\"\nexport { TryFallbackChain } from \"./try-fallback-chain\"\n\n// ---------------------------------------------------------------------------\n// position-area state derivation (value string ⇄ grid state)\n// ---------------------------------------------------------------------------\n\nfunction deriveAreaState(value: string): PositionAreaState {\n  const { keywords } = parsePositionArea(value)\n  const system: PositionAreaState[\"system\"] = keywords.some(\n    (k) => k.includes(\"block\") || k.includes(\"inline\"),\n  )\n    ? \"logical\"\n    : \"physical\"\n  const span =\n    keywords.length > 0 &&\n    keywords.every((k) => k.startsWith(\"span-\")) &&\n    !keywords.includes(\"span-all\")\n  // Strip span / logical down to the bare physical keyword for the cell lookup.\n  const bare = keywords.map((k) => {\n    let s = k.startsWith(\"span-\") ? k.slice(\"span-\".length) : k\n    s = s\n      .replace(\"block-start\", \"top\")\n      .replace(\"block-end\", \"bottom\")\n      .replace(\"inline-start\", \"left\")\n      .replace(\"inline-end\", \"right\")\n    return s\n  })\n  const cell = keywordsToCell(bare) ?? { row: 1, col: 1 }\n  return { system, span, row: cell.row, col: cell.col }\n}\n\n// ---------------------------------------------------------------------------\n// Props\n// ---------------------------------------------------------------------------\n\nexport interface AnchorPositionEditorPanelProps {\n  value: AnchorPositionString | (string & {})\n  onChange: (value: AnchorPositionString) => void\n  /** `\"position-area\"` (default), `\"anchor\"`, or `\"position-try\"`. */\n  mode?: AnchorPositionMode\n  className?: string\n  \"aria-label\"?: string\n}\n\nexport interface AnchorPositionEditorProps\n  extends AnchorPositionEditorPanelProps {}\n\n// ---------------------------------------------------------------------------\n// AnchorPositionEditor — popover-wrapped\n// ---------------------------------------------------------------------------\n\nexport function AnchorPositionEditor(props: AnchorPositionEditorProps) {\n  const {\n    value,\n    mode = \"position-area\",\n    className,\n    \"aria-label\": ariaLabel = \"Edit a CSS anchor-positioning value\",\n  } = props\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 className=\"text-[10px] text-muted-foreground uppercase\">\n            {mode}\n          </span>\n          <span className=\"max-w-[220px] truncate text-xs\">{value}</span>\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent className=\"w-auto p-0\" align=\"start\">\n        <AnchorPositionEditorPanel {...props} />\n      </PopoverContent>\n    </Popover>\n  )\n}\n\n// ---------------------------------------------------------------------------\n// AnchorPositionEditorPanel — inline\n// ---------------------------------------------------------------------------\n\nexport function AnchorPositionEditorPanel({\n  value,\n  onChange,\n  mode = \"position-area\",\n  className,\n  \"aria-label\": ariaLabel = \"CSS anchor-positioning editor\",\n}: AnchorPositionEditorPanelProps) {\n  const lastEmittedRef = useRef<string | null>(null)\n\n  // Per-mode editor state, seeded from the incoming value.\n  const [areaState, setAreaState] = useState<PositionAreaState>(() =>\n    deriveAreaState(String(value)),\n  )\n  const [anchorExpr, setAnchorExpr] = useState<AnchorExpr>(\n    () =>\n      parseAnchor(String(value)) ?? {\n        fn: \"anchor\",\n        name: \"--anchor\",\n        side: \"bottom\",\n      },\n  )\n  const [tryChain, setTryChain] = useState<TryFallback[]>(() =>\n    parsePositionTry(String(value)),\n  )\n\n  // Resync from an external value or a mode change (skip our own emits).\n  useEffect(() => {\n    if (value === lastEmittedRef.current) return\n    const v = String(value)\n    if (mode === \"position-area\") setAreaState(deriveAreaState(v))\n    else if (mode === \"anchor\") {\n      const parsed = parseAnchor(v)\n      if (parsed) setAnchorExpr(parsed)\n    } else setTryChain(parsePositionTry(v))\n  }, [value, mode])\n\n  const commit = (str: string) => {\n    lastEmittedRef.current = str\n    onChange(str as AnchorPositionString)\n  }\n\n  const commitArea = (next: PositionAreaState) => {\n    setAreaState(next)\n    commit(formatPositionArea(stateToKeywords(next)))\n  }\n  const commitAnchor = (next: AnchorExpr) => {\n    setAnchorExpr(next)\n    commit(formatAnchor(next))\n  }\n  const commitTry = (next: TryFallback[]) => {\n    setTryChain(next)\n    commit(formatPositionTry(next))\n  }\n\n  return (\n    <fieldset\n      className={cn(\n        \"m-0 w-[420px] space-y-3 border-0 bg-background p-3\",\n        className,\n      )}\n      aria-label={ariaLabel}\n    >\n      {mode === \"position-area\" && (\n        <>\n          <PositionAreaGrid\n            system={areaState.system}\n            span={areaState.span}\n            row={areaState.row}\n            col={areaState.col}\n            onChange={commitArea}\n          />\n          <code className=\"block overflow-x-auto rounded bg-muted/50 px-2 py-1.5 font-mono text-foreground text-xs\">\n            {formatPositionArea(stateToKeywords(areaState)) || \" \"}\n          </code>\n        </>\n      )}\n\n      {mode === \"anchor\" && (\n        <>\n          <AnchorExprFields expr={anchorExpr} onChange={commitAnchor} />\n          <code className=\"block overflow-x-auto rounded bg-muted/50 px-2 py-1.5 font-mono text-foreground text-xs\">\n            {formatAnchor(anchorExpr) || \" \"}\n          </code>\n          <AnchorPreview value=\"center\" />\n        </>\n      )}\n\n      {mode === \"position-try\" && (\n        <TryFallbackChain fallbacks={tryChain} onChange={commitTry} />\n      )}\n    </fieldset>\n  )\n}\n\n// ---------------------------------------------------------------------------\n// LiveString — the produced value in a `<code>` (internal helper, exported\n// for parity with the sibling sub-components and demos).\n// ---------------------------------------------------------------------------\n\nexport function LiveString({ value }: { value: string }) {\n  return (\n    <code className=\"block overflow-x-auto rounded bg-muted/50 px-2 py-1.5 font-mono text-foreground text-xs\">\n      {value || \" \"}\n    </code>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/anchor-position-editor/anchor-position-editor.tsx"
    },
    {
      "path": "src/components/ui/anchor-position-editor/anchor-position-editor.types.ts",
      "content": "// =====================================================================\n// anchor-position-editor.types.ts — ridiculously typed CSS\n// anchor-positioning values.\n//\n// Three strict validators behind one `mode` prop:\n//   • PositionAreaLiteral<S> — the `position-area` keyword pair, with the\n//     CROSS-AXIS rule (two keywords must sit on different axes of the SAME\n//     coordinate system; physical & logical never mix; center / span-all\n//     are system-neutral). This positional-tuple constraint — rejecting a\n//     PAIR for sharing an axis — is new to the registry.\n//   • AnchorLiteral<S> — `anchor()` / `anchor-size()` inset/size functions.\n//   • PositionTryLiteral<S> — a `position-try-fallbacks` comma list.\n//\n// Built entirely on ridiculous-type-kit. See the design spec\n// docs/superpowers/specs/2026-06-19-anchor-position-editor-design.md.\n// =====================================================================\n\nimport type {\n  And,\n  IsLength,\n  IsPercentage,\n  KeepIf,\n  Or,\n  ParseFunction,\n  SplitByComma,\n  SplitBySpace,\n  StartsWith,\n} from \"@/lib/ridiculous-type-kit\"\n\n// ---------------------------------------------------------------------------\n// position-area keyword vocabulary, grouped by axis family\n// ---------------------------------------------------------------------------\n\n/** Physical x-axis keywords (and their `span-` reach forms). */\ntype PhysX =\n  | \"left\"\n  | \"right\"\n  | \"x-start\"\n  | \"x-end\"\n  | \"x-self-start\"\n  | \"x-self-end\"\n  | \"span-left\"\n  | \"span-right\"\n  | \"span-x-start\"\n  | \"span-x-end\"\n  | \"span-x-self-start\"\n  | \"span-x-self-end\"\n\n/** Physical y-axis keywords (and their `span-` reach forms). */\ntype PhysY =\n  | \"top\"\n  | \"bottom\"\n  | \"y-start\"\n  | \"y-end\"\n  | \"y-self-start\"\n  | \"y-self-end\"\n  | \"span-top\"\n  | \"span-bottom\"\n  | \"span-y-start\"\n  | \"span-y-end\"\n  | \"span-y-self-start\"\n  | \"span-y-self-end\"\n\n/** Logical block-axis keywords. */\ntype LogBlock =\n  | \"block-start\"\n  | \"block-end\"\n  | \"self-block-start\"\n  | \"self-block-end\"\n  | \"span-block-start\"\n  | \"span-block-end\"\n  | \"span-self-block-start\"\n  | \"span-self-block-end\"\n\n/** Logical inline-axis keywords. */\ntype LogInline =\n  | \"inline-start\"\n  | \"inline-end\"\n  | \"self-inline-start\"\n  | \"self-inline-end\"\n  | \"span-inline-start\"\n  | \"span-inline-end\"\n  | \"span-self-inline-start\"\n  | \"span-self-inline-end\"\n\n/**\n * System-neutral keywords: usable on either axis / either coordinate system.\n * The \"ambiguous-axis\" forms (start/end/self-start/self-end) live here too —\n * their real axis depends on writing-mode, undecidable at the type level, so\n * the strict tier treats them leniently (A6 in the spec).\n */\ntype Neutral =\n  | \"center\"\n  | \"span-all\"\n  | \"start\"\n  | \"end\"\n  | \"self-start\"\n  | \"self-end\"\n  | \"span-start\"\n  | \"span-end\"\n  | \"span-self-start\"\n  | \"span-self-end\"\n\n/** Every valid `position-area` keyword. */\nexport type PaKeyword = PhysX | PhysY | LogBlock | LogInline | Neutral\n\n/** The axis a keyword binds to (or `neutral` for system-agnostic forms). */\nexport type PositionAxis = \"x\" | \"y\" | \"block\" | \"inline\" | \"neutral\"\n\n/** Map a `position-area` keyword to its axis tag. */\nexport type AxisOf<K extends string> = K extends PhysX\n  ? \"x\"\n  : K extends PhysY\n    ? \"y\"\n    : K extends LogBlock\n      ? \"block\"\n      : K extends LogInline\n        ? \"inline\"\n        : K extends Neutral\n          ? \"neutral\"\n          : never\n\n/**\n * The cross-axis compatibility rule: two keywords pair iff they are on\n * different axes of the SAME coordinate system. `neutral` pairs with\n * anything; x↔y (physical) and block↔inline (logical) pair; everything\n * else (same axis, or physical↔logical) is rejected.\n */\nexport type Compatible<\n  A extends PositionAxis,\n  B extends PositionAxis,\n> = A extends \"neutral\"\n  ? true\n  : B extends \"neutral\"\n    ? true\n    : A extends \"x\"\n      ? B extends \"y\"\n        ? true\n        : false\n      : A extends \"y\"\n        ? B extends \"x\"\n          ? true\n          : false\n        : A extends \"block\"\n          ? B extends \"inline\"\n            ? true\n            : false\n          : A extends \"inline\"\n            ? B extends \"block\"\n              ? true\n              : false\n            : false\n\ntype ValidatePaBool<Toks extends string[]> = Toks extends [\n  infer A extends string,\n]\n  ? A extends PaKeyword\n    ? true\n    : false\n  : Toks extends [infer A extends string, infer B extends string]\n    ? A extends PaKeyword\n      ? B extends PaKeyword\n        ? Compatible<AxisOf<A>, AxisOf<B>>\n        : false\n      : false\n    : false\n\n/** Strict validator for a `position-area` value. Resolves to `S` or `never`. */\nexport type PositionAreaLiteral<S extends string> = KeepIf<\n  ValidatePaBool<SplitBySpace<S>>,\n  S\n>\n\n// ---------------------------------------------------------------------------\n// anchor() / anchor-size()\n// ---------------------------------------------------------------------------\n\n/** Keywords accepted as an `anchor()` side. */\nexport type AnchorSideKeyword =\n  | \"top\"\n  | \"left\"\n  | \"right\"\n  | \"bottom\"\n  | \"start\"\n  | \"end\"\n  | \"self-start\"\n  | \"self-end\"\n  | \"center\"\n  | \"inside\"\n  | \"outside\"\n\n/** Keywords accepted as an `anchor-size()` dimension. */\nexport type AnchorSizeKeyword =\n  | \"width\"\n  | \"height\"\n  | \"block\"\n  | \"inline\"\n  | \"self-block\"\n  | \"self-inline\"\n\ntype IsLengthPct<S extends string> = Or<IsLength<S>, IsPercentage<S>>\n\ntype IsAnchorSide<S extends string> = Or<\n  S extends AnchorSideKeyword ? true : false,\n  IsPercentage<S>\n>\n\ntype IsAnchorSize<S extends string> = S extends AnchorSizeKeyword ? true : false\n\ntype IsAnchorHead<Head extends string, Fn extends \"anchor\" | \"anchor-size\"> =\n  SplitBySpace<Head> extends [infer Side extends string]\n    ? Fn extends \"anchor\"\n      ? IsAnchorSide<Side>\n      : IsAnchorSize<Side>\n    : SplitBySpace<Head> extends [\n          infer Name extends string,\n          infer Side extends string,\n        ]\n      ? And<\n          StartsWith<Name, \"--\">,\n          Fn extends \"anchor\" ? IsAnchorSide<Side> : IsAnchorSize<Side>\n        >\n      : false\n\ntype IsAnchorArgs<Args extends string, Fn extends \"anchor\" | \"anchor-size\"> =\n  SplitByComma<Args> extends [infer Head extends string]\n    ? IsAnchorHead<Head, Fn>\n    : SplitByComma<Args> extends [\n          infer Head extends string,\n          infer Fallback extends string,\n        ]\n      ? And<IsAnchorHead<Head, Fn>, IsLengthPct<Fallback>>\n      : false\n\ntype IsAnchor<S extends string> =\n  ParseFunction<S> extends {\n    name: infer N extends string\n    args: infer Args extends string\n  }\n    ? N extends \"anchor\"\n      ? IsAnchorArgs<Args, \"anchor\">\n      : N extends \"anchor-size\"\n        ? IsAnchorArgs<Args, \"anchor-size\">\n        : false\n    : false\n\n/** Strict validator for an `anchor()` / `anchor-size()` value. */\nexport type AnchorLiteral<S extends string> = KeepIf<IsAnchor<S>, S>\n\n// ---------------------------------------------------------------------------\n// position-try-fallbacks\n// ---------------------------------------------------------------------------\n\n/** The three `<try-tactic>` keywords. */\nexport type TryTactic = \"flip-block\" | \"flip-inline\" | \"flip-start\"\n\ntype IsIdentOrTactic<T extends string> = Or<\n  StartsWith<T, \"--\">,\n  T extends TryTactic ? true : false\n>\n\ntype AllIdentOrTactic<Toks extends string[]> = Toks extends [\n  infer H extends string,\n  ...infer R extends string[],\n]\n  ? IsIdentOrTactic<H> extends true\n    ? AllIdentOrTactic<R>\n    : false\n  : true\n\ntype IsTryFallbackStr<F extends string> = F extends \"none\"\n  ? true\n  : SplitBySpace<F> extends infer Toks extends string[]\n    ? Toks extends []\n      ? false\n      : AllIdentOrTactic<Toks> extends true\n        ? true\n        : ValidatePaBool<Toks>\n    : false\n\ntype AllTryFallbacks<Toks extends string[]> = Toks extends [\n  infer H extends string,\n  ...infer R extends string[],\n]\n  ? IsTryFallbackStr<H> extends true\n    ? AllTryFallbacks<R>\n    : false\n  : true\n\n/** Strict validator for a `position-try-fallbacks` value. */\nexport type PositionTryLiteral<S extends string> =\n  SplitByComma<S> extends infer Toks extends string[]\n    ? Toks extends []\n      ? never\n      : KeepIf<AllTryFallbacks<Toks>, S>\n    : never\n\n// ---------------------------------------------------------------------------\n// Call-site helpers (resolve invalid input to `never` at the argument)\n// ---------------------------------------------------------------------------\n\nexport const cssPositionArea = <S extends string>(\n  value: S & PositionAreaLiteral<S>,\n): S => value\n\nexport const cssAnchor = <S extends string>(value: S & AnchorLiteral<S>): S =>\n  value\n\nexport const cssPositionTry = <S extends string>(\n  value: S & PositionTryLiteral<S>,\n): S => value\n\n// ---------------------------------------------------------------------------\n// IntelliSense suggestion strings + mode map\n// ---------------------------------------------------------------------------\n\nexport type AnchorPositionMode = \"position-area\" | \"anchor\" | \"position-try\"\n\nexport type PositionAreaString =\n  | PaKeyword\n  | `${PaKeyword} ${PaKeyword}`\n  | (string & {})\n\nexport type AnchorString =\n  | `anchor(${string})`\n  | `anchor-size(${string})`\n  | (string & {})\n\nexport type PositionTryString = string & {}\n\nexport type AnchorPositionString =\n  | PositionAreaString\n  | AnchorString\n  | PositionTryString\n\nexport interface AnchorStringMap {\n  \"position-area\": PositionAreaString\n  anchor: AnchorString\n  \"position-try\": PositionTryString\n}\n\n// ---------------------------------------------------------------------------\n// Utility types\n// ---------------------------------------------------------------------------\n\n/** The keyword tuple of a `position-area` value. */\nexport type KeywordsOf<S extends string> = SplitBySpace<S>\n\n// ---------------------------------------------------------------------------\n// Internal editor state (exported for advanced / custom serialization)\n// ---------------------------------------------------------------------------\n\nexport interface AnchorExpr {\n  fn: \"anchor\" | \"anchor-size\"\n  name?: string\n  side: string\n  fallback?: string\n}\n\nexport interface TryFallback {\n  kind: \"none\" | \"area\" | \"tactics\"\n  area?: string\n  idents?: string[]\n  tactics?: string[]\n}\n\nexport interface PositionAreaState {\n  system: \"physical\" | \"logical\"\n  span: boolean\n  row: 0 | 1 | 2\n  col: 0 | 1 | 2\n}\n",
      "type": "registry:ui",
      "target": "components/ui/anchor-position-editor/anchor-position-editor.types.ts"
    },
    {
      "path": "src/components/ui/anchor-position-editor/anchor-position-editor.helpers.ts",
      "content": "// =====================================================================\n// anchor-position-editor.helpers.ts\n//\n// Pure runtime parse / format for CSS anchor-positioning values. This is the\n// SUPERSET of the strict type tier: it parses the structure (the position-area\n// keyword pair, the anchor()/anchor-size() function, the position-try fallback\n// chain) and surfaces the cross-axis verdict, but keeps literals the UI needs\n// to round-trip (mirrors query-builder.helpers.ts). The single source the UI\n// drives off.\n//\n// `KEYWORD_TABLE` below is the runtime twin of the type `AxisOf` in\n// anchor-position-editor.types.ts — separately authored, reviewed together (the\n// query-builder precedent). The same example keywords appear in both the type-\n// test and the parse-test, so any drift fails a test.\n// =====================================================================\n\nimport type {\n  AnchorExpr,\n  AnchorPositionMode,\n  PositionAxis,\n  TryFallback,\n} from \"./anchor-position-editor.types\"\n\n// ---------------------------------------------------------------------------\n// KEYWORD_TABLE — the position-area keyword vocabulary, grouped by axis family.\n// Runtime mirror of the PhysX / PhysY / LogBlock / LogInline / Neutral unions.\n// ---------------------------------------------------------------------------\n\nconst PHYS_X: readonly string[] = [\n  \"left\",\n  \"right\",\n  \"x-start\",\n  \"x-end\",\n  \"x-self-start\",\n  \"x-self-end\",\n  \"span-left\",\n  \"span-right\",\n  \"span-x-start\",\n  \"span-x-end\",\n  \"span-x-self-start\",\n  \"span-x-self-end\",\n]\n\nconst PHYS_Y: readonly string[] = [\n  \"top\",\n  \"bottom\",\n  \"y-start\",\n  \"y-end\",\n  \"y-self-start\",\n  \"y-self-end\",\n  \"span-top\",\n  \"span-bottom\",\n  \"span-y-start\",\n  \"span-y-end\",\n  \"span-y-self-start\",\n  \"span-y-self-end\",\n]\n\nconst LOG_BLOCK: readonly string[] = [\n  \"block-start\",\n  \"block-end\",\n  \"self-block-start\",\n  \"self-block-end\",\n  \"span-block-start\",\n  \"span-block-end\",\n  \"span-self-block-start\",\n  \"span-self-block-end\",\n]\n\nconst LOG_INLINE: readonly string[] = [\n  \"inline-start\",\n  \"inline-end\",\n  \"self-inline-start\",\n  \"self-inline-end\",\n  \"span-inline-start\",\n  \"span-inline-end\",\n  \"span-self-inline-start\",\n  \"span-self-inline-end\",\n]\n\nconst NEUTRAL: readonly string[] = [\n  \"center\",\n  \"span-all\",\n  \"start\",\n  \"end\",\n  \"self-start\",\n  \"self-end\",\n  \"span-start\",\n  \"span-end\",\n  \"span-self-start\",\n  \"span-self-end\",\n]\n\nconst KEYWORD_TABLE: Record<PositionAxis, readonly string[]> = {\n  x: PHYS_X,\n  y: PHYS_Y,\n  block: LOG_BLOCK,\n  inline: LOG_INLINE,\n  neutral: NEUTRAL,\n}\n\nconst ALL_PA_KEYWORDS: readonly string[] = [\n  ...PHYS_X,\n  ...PHYS_Y,\n  ...LOG_BLOCK,\n  ...LOG_INLINE,\n  ...NEUTRAL,\n]\n\nconst ANCHOR_SIDES: readonly string[] = [\n  \"top\",\n  \"left\",\n  \"right\",\n  \"bottom\",\n  \"start\",\n  \"end\",\n  \"self-start\",\n  \"self-end\",\n  \"center\",\n  \"inside\",\n  \"outside\",\n]\n\nconst ANCHOR_SIZES: readonly string[] = [\n  \"width\",\n  \"height\",\n  \"block\",\n  \"inline\",\n  \"self-block\",\n  \"self-inline\",\n]\n\nconst TRY_TACTICS: readonly string[] = [\n  \"flip-block\",\n  \"flip-inline\",\n  \"flip-start\",\n]\n\n// ---------------------------------------------------------------------------\n// axisOf / areCompatible — runtime mirrors of the type AxisOf / Compatible\n// ---------------------------------------------------------------------------\n\n/**\n * The axis a `position-area` keyword binds to — runtime mirror of the type\n * `AxisOf`. An unrecognized keyword is `\"unknown\"` (the runtime escape hatch\n * the type expresses as `never`).\n */\nexport function axisOf(keyword: string): PositionAxis | \"unknown\" {\n  const k = keyword.trim()\n  for (const axis of [\"x\", \"y\", \"block\", \"inline\", \"neutral\"] as const) {\n    if (KEYWORD_TABLE[axis].includes(k)) return axis\n  }\n  return \"unknown\"\n}\n\n/**\n * Whether two `position-area` keywords pair — runtime mirror of the type\n * `Compatible`. `neutral` pairs with anything; x↔y (physical) and block↔inline\n * (logical) pair; same-axis and physical↔logical are rejected; an unknown\n * keyword is incompatible.\n */\nexport function areCompatible(a: string, b: string): boolean {\n  const ax = axisOf(a)\n  const bx = axisOf(b)\n  if (ax === \"unknown\" || bx === \"unknown\") return false\n  if (ax === \"neutral\" || bx === \"neutral\") return true\n  if (ax === \"x\") return bx === \"y\"\n  if (ax === \"y\") return bx === \"x\"\n  if (ax === \"block\") return bx === \"inline\"\n  if (ax === \"inline\") return bx === \"block\"\n  return false\n}\n\n// ---------------------------------------------------------------------------\n// parsePositionArea — string → keyword tokens + cross-axis verdict\n// ---------------------------------------------------------------------------\n\n/**\n * Tokenize a `position-area` value into its keyword(s) and surface the verdict\n * as `error` (`null` when valid). Keeps the raw tokens even on error so the UI\n * can show what was typed. Rejects empty input, more than two tokens, an\n * unknown keyword, and an incompatible pair (same axis / physical↔logical).\n */\nexport function parsePositionArea(src: string): {\n  keywords: string[]\n  error: string | null\n} {\n  const keywords = src\n    .trim()\n    .split(/\\s+/)\n    .filter((t) => t.length > 0)\n  if (keywords.length === 0) {\n    return { keywords: [], error: \"empty position-area\" }\n  }\n  if (keywords.length > 2) {\n    return { keywords, error: \"a position-area takes one or two keywords\" }\n  }\n  for (const k of keywords) {\n    if (axisOf(k) === \"unknown\") {\n      return { keywords, error: `unknown keyword: ${k}` }\n    }\n  }\n  if (keywords.length === 2 && !areCompatible(keywords[0], keywords[1])) {\n    return {\n      keywords,\n      error: \"the two keywords must be on different axes of the same system\",\n    }\n  }\n  return { keywords, error: null }\n}\n\n// ---------------------------------------------------------------------------\n// parseAnchor — string → AnchorExpr | null\n// ---------------------------------------------------------------------------\n\nfunction splitFunction(src: string): { name: string; args: string } | null {\n  const s = src.trim()\n  const open = s.indexOf(\"(\")\n  if (open === -1 || !s.endsWith(\")\")) return null\n  const name = s.slice(0, open).trim()\n  if (name === \"\") return null\n  const args = s.slice(open + 1, -1).trim()\n  return { name, args }\n}\n\n/**\n * Parse an `anchor()` / `anchor-size()` value into an `AnchorExpr`, or `null`\n * if it is not one of those functions / has empty args. The optional leading\n * `--name` ident, the side/size keyword (or a bare percentage), and the\n * optional `<length-percentage>` fallback are split structurally — keyword\n * gating is the strict tier's job, not the parser's.\n */\nexport function parseAnchor(src: string): AnchorExpr | null {\n  const fn = splitFunction(src)\n  if (fn === null) return null\n  if (fn.name !== \"anchor\" && fn.name !== \"anchor-size\") return null\n  if (fn.args === \"\") return null\n\n  const parts = fn.args.split(\",\").map((p) => p.trim())\n  if (parts.length > 2) return null\n  const head = parts[0]\n  const fallback = parts[1]\n  if (head === \"\") return null\n  if (parts.length === 2 && (fallback === undefined || fallback === \"\")) {\n    return null\n  }\n\n  const headTokens = head.split(/\\s+/).filter((t) => t.length > 0)\n  let name: string | undefined\n  let side: string\n  if (headTokens.length === 1) {\n    side = headTokens[0]\n  } else if (headTokens.length === 2) {\n    name = headTokens[0]\n    side = headTokens[1]\n  } else {\n    return null\n  }\n\n  const expr: AnchorExpr = { fn: fn.name, side }\n  if (name !== undefined) expr.name = name\n  if (fallback !== undefined && fallback !== \"\") expr.fallback = fallback\n  return expr\n}\n\n// ---------------------------------------------------------------------------\n// parsePositionTry — string → TryFallback[]\n// ---------------------------------------------------------------------------\n\nfunction classifyFallback(raw: string): TryFallback {\n  const f = raw.trim()\n  if (f === \"none\") return { kind: \"none\" }\n\n  const tokens = f.split(/\\s+/).filter((t) => t.length > 0)\n  const idents = tokens.filter((t) => t.startsWith(\"--\"))\n  const tactics = tokens.filter((t) => TRY_TACTICS.includes(t))\n  // The `<dashed-ident> || <try-tactic>` arm — every token is an ident or a\n  // tactic.\n  if (idents.length + tactics.length === tokens.length && tokens.length > 0) {\n    return { kind: \"tactics\", idents, tactics }\n  }\n  // Otherwise interpret the whole fallback as a <position-area>.\n  return { kind: \"area\", area: f }\n}\n\n/**\n * Parse a `position-try-fallbacks` value into its fallback chain. Each comma-\n * separated fallback is `none`, a `<dashed-ident> || <try-tactic>` arm\n * (`tactics`), or a `<position-area>` (`area`). An empty string yields `[]`.\n */\nexport function parsePositionTry(src: string): TryFallback[] {\n  const trimmed = src.trim()\n  if (trimmed === \"\") return []\n  return trimmed\n    .split(\",\")\n    .map((f) => f.trim())\n    .filter((f) => f.length > 0)\n    .map(classifyFallback)\n}\n\n// ---------------------------------------------------------------------------\n// format* — canonical re-serialization\n// ---------------------------------------------------------------------------\n\n/**\n * Serialize a `position-area` keyword list. A duplicate `center` pair collapses\n * to the single keyword `center` (the middle-cell convention); an empty list\n * is the empty string.\n */\nexport function formatPositionArea(keywords: string[]): string {\n  if (keywords.length === 0) return \"\"\n  if (\n    keywords.length === 2 &&\n    keywords[0] === \"center\" &&\n    keywords[1] === \"center\"\n  ) {\n    return \"center\"\n  }\n  return keywords.join(\" \")\n}\n\n/** Serialize an `AnchorExpr` back to its `anchor(...)` / `anchor-size(...)`. */\nexport function formatAnchor(expr: AnchorExpr): string {\n  const head = expr.name ? `${expr.name} ${expr.side}` : expr.side\n  const args = expr.fallback ? `${head}, ${expr.fallback}` : head\n  return `${expr.fn}(${args})`\n}\n\nfunction formatFallback(f: TryFallback): string {\n  if (f.kind === \"none\") return \"none\"\n  if (f.kind === \"area\") return f.area ?? \"\"\n  const idents = f.idents ?? []\n  const tactics = f.tactics ?? []\n  return [...idents, ...tactics].join(\" \")\n}\n\n/** Serialize a `position-try-fallbacks` chain to its comma list. */\nexport function formatPositionTry(fallbacks: TryFallback[]): string {\n  return fallbacks.map(formatFallback).join(\", \")\n}\n\n// ---------------------------------------------------------------------------\n// option sources (<select> data)\n// ---------------------------------------------------------------------------\n\n/** Every known `position-area` keyword (the strict whitelist). */\nexport function positionAreaKeywords(): readonly string[] {\n  return ALL_PA_KEYWORDS\n}\n\n/** The `anchor()` side keywords. */\nexport function anchorSides(): readonly string[] {\n  return ANCHOR_SIDES\n}\n\n/** The `anchor-size()` dimension keywords. */\nexport function anchorSizes(): readonly string[] {\n  return ANCHOR_SIZES\n}\n\n/** The three `<try-tactic>` keywords. */\nexport function tryTactics(): readonly string[] {\n  return TRY_TACTICS\n}\n\n// ---------------------------------------------------------------------------\n// The 3×3 grid ⇄ keyword-pair mapping (physical default; §4.1)\n// ---------------------------------------------------------------------------\n\nconst ROW_KEYWORDS = [\"top\", \"center\", \"bottom\"] as const\nconst COL_KEYWORDS = [\"left\", \"center\", \"right\"] as const\n\ntype Cell = 0 | 1 | 2\n\n/**\n * The physical keyword pair for a grid cell. The `[row, col]` keywords are the\n * y-axis (top/center/bottom) and x-axis (left/center/right) of the cell. The\n * center cell (`1,1`) yields the `[center, center]` pair, which\n * `formatPositionArea` collapses to the single keyword `center`.\n */\nexport function cellToKeywords(row: Cell, col: Cell): [string, string] {\n  return [ROW_KEYWORDS[row], COL_KEYWORDS[col]]\n}\n\n/** Map a keyword to its grid row (0..2), via the y / block axis, or null. */\nfunction rowOf(keyword: string): Cell | null {\n  switch (keyword) {\n    case \"top\":\n    case \"block-start\":\n      return 0\n    case \"center\":\n      return 1\n    case \"bottom\":\n    case \"block-end\":\n      return 2\n    default:\n      return null\n  }\n}\n\n/** Map a keyword to its grid column (0..2), via the x / inline axis, or null. */\nfunction colOf(keyword: string): Cell | null {\n  switch (keyword) {\n    case \"left\":\n    case \"inline-start\":\n      return 0\n    case \"center\":\n      return 1\n    case \"right\":\n    case \"inline-end\":\n      return 2\n    default:\n      return null\n  }\n}\n\n/**\n * Map a `position-area` keyword pair (or the single `center`) back to its grid\n * cell. Order-insensitive: the row keyword and the column keyword may appear in\n * either order. Logical pairs (`block-*` / `inline-*`) map onto the same cells\n * as their physical counterparts. Returns `null` for an unmappable pair.\n */\nexport function keywordsToCell(\n  keywords: string[],\n): { row: Cell; col: Cell } | null {\n  if (keywords.length === 1) {\n    if (keywords[0] === \"center\") return { row: 1, col: 1 }\n    return null\n  }\n  if (keywords.length !== 2) return null\n  const [a, b] = keywords\n  // Try (a = row, b = col), then the swapped order.\n  for (const [rowKw, colKw] of [\n    [a, b],\n    [b, a],\n  ] as const) {\n    const row = rowOf(rowKw)\n    const col = colOf(colKw)\n    if (row !== null && col !== null) return { row, col }\n  }\n  return null\n}\n\n// ---------------------------------------------------------------------------\n// defaults\n// ---------------------------------------------------------------------------\n\n/** A sensible seed value per mode. */\nexport function defaultFor(mode: AnchorPositionMode): string {\n  switch (mode) {\n    case \"position-area\":\n      return \"center\"\n    case \"anchor\":\n      return \"anchor(--anchor bottom)\"\n    case \"position-try\":\n      return \"flip-block\"\n  }\n}\n",
      "type": "registry:ui",
      "target": "components/ui/anchor-position-editor/anchor-position-editor.helpers.ts"
    },
    {
      "path": "src/components/ui/anchor-position-editor/mini-select.tsx",
      "content": "\"use client\"\n\nimport type { ReactNode } from \"react\"\nimport { cn } from \"@/lib/utils\"\n\n// ---------------------------------------------------------------------------\n// MiniSelect — the compact `<select>` chrome shared by every anchor-position-\n// editor dropdown (function, side/size, tactic, fallback kind). Owns the one\n// class-string so the controls never drift. A LOCAL copy: registry\n// self-containment means this component carries its own MiniSelect rather than\n// importing query-builder's (`shadcn add` must pull a self-contained tree).\n// `onValueChange` hands back the raw `e.target.value`.\n// ---------------------------------------------------------------------------\n\nexport const selectClass =\n  \"h-8 rounded-md border border-input bg-background px-1 font-mono text-xs\"\n\nexport interface MiniSelectProps {\n  \"aria-label\": string\n  value: string\n  onValueChange: (value: string) => void\n  children: ReactNode\n  className?: string\n}\n\nexport function MiniSelect({\n  \"aria-label\": ariaLabel,\n  value,\n  onValueChange,\n  children,\n  className,\n}: MiniSelectProps) {\n  return (\n    <select\n      aria-label={ariaLabel}\n      value={value}\n      onChange={(e) => onValueChange(e.target.value)}\n      className={cn(selectClass, className)}\n    >\n      {children}\n    </select>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/anchor-position-editor/mini-select.tsx"
    },
    {
      "path": "src/components/ui/anchor-position-editor/position-area-grid.tsx",
      "content": "\"use client\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n  cellToKeywords,\n  formatPositionArea,\n} from \"./anchor-position-editor.helpers\"\nimport type { PositionAreaState } from \"./anchor-position-editor.types\"\nimport { AnchorPreview } from \"./anchor-preview\"\n\n// ---------------------------------------------------------------------------\n// PositionAreaGrid (public) — the 3×3 clickable placement grid à la\n// grid-builder's AreasPainter. Each cell is a labelled `<button>` (the\n// stable physical name, e.g. \"place at top left\") with `aria-pressed`. A\n// logical/physical header toggle swaps the EMITTED vocabulary to block/inline\n// for the same cells; a span toggle switches the emitted keywords to their\n// `span-` reach forms. Editing one coordinate system at a time guarantees the\n// cross-axis type rule by construction — the grid never mixes systems.\n// A live mini snap-preview (AnchorPreview) reflects the current cell.\n// ---------------------------------------------------------------------------\n\ntype Cell = 0 | 1 | 2\n\nconst ROW_LABELS = [\"top\", \"center\", \"bottom\"] as const\nconst COL_LABELS = [\"left\", \"center\", \"right\"] as const\n\n/** Re-express a physical keyword in the logical (block/inline) vocabulary. */\nfunction toLogical(keyword: string): string {\n  switch (keyword) {\n    case \"top\":\n      return \"block-start\"\n    case \"bottom\":\n      return \"block-end\"\n    case \"left\":\n      return \"inline-start\"\n    case \"right\":\n      return \"inline-end\"\n    default:\n      return keyword // center stays neutral\n  }\n}\n\n/** Apply the `span-` reach prefix (center / neutral keywords stay as-is). */\nfunction toSpan(keyword: string): string {\n  if (keyword === \"center\") return keyword\n  return `span-${keyword}`\n}\n\n/**\n * The emitted keyword list for a grid state — the row+col pair mapped through\n * the system + span toggles, then collapsed (`center center` → `center`).\n */\nexport function stateToKeywords(state: PositionAreaState): string[] {\n  let [rowKw, colKw] = cellToKeywords(state.row, state.col)\n  if (state.system === \"logical\") {\n    rowKw = toLogical(rowKw)\n    colKw = toLogical(colKw)\n  }\n  if (state.span) {\n    rowKw = toSpan(rowKw)\n    colKw = toSpan(colKw)\n  }\n  return [rowKw, colKw]\n}\n\nexport interface PositionAreaGridProps {\n  system: \"physical\" | \"logical\"\n  span: boolean\n  row: Cell\n  col: Cell\n  onChange: (next: PositionAreaState) => void\n  className?: string\n}\n\nexport function PositionAreaGrid({\n  system,\n  span,\n  row,\n  col,\n  onChange,\n  className,\n}: PositionAreaGridProps) {\n  const state: PositionAreaState = { system, span, row, col }\n  const live = formatPositionArea(stateToKeywords(state))\n\n  const pick = (r: Cell, c: Cell) => onChange({ ...state, row: r, col: c })\n\n  return (\n    <div className={cn(\"space-y-3\", className)}>\n      <div className=\"flex flex-wrap items-center gap-2 text-xs\">\n        <label className=\"flex h-8 items-center gap-1.5 rounded-md border border-input px-2 font-mono text-muted-foreground\">\n          <input\n            type=\"checkbox\"\n            aria-label=\"use the logical (block / inline) coordinate system\"\n            checked={system === \"logical\"}\n            onChange={(e) =>\n              onChange({\n                ...state,\n                system: e.target.checked ? \"logical\" : \"physical\",\n              })\n            }\n            className=\"size-3.5\"\n          />\n          logical\n        </label>\n        <label className=\"flex h-8 items-center gap-1.5 rounded-md border border-input px-2 font-mono text-muted-foreground\">\n          <input\n            type=\"checkbox\"\n            aria-label=\"span the chosen edges (span- reach)\"\n            checked={span}\n            onChange={(e) => onChange({ ...state, span: e.target.checked })}\n            className=\"size-3.5\"\n          />\n          span\n        </label>\n      </div>\n\n      <div data-position-area-grid className=\"grid w-fit grid-cols-3 gap-1\">\n        {([0, 1, 2] as const).map((r) =>\n          ([0, 1, 2] as const).map((c) => {\n            const label = `place at ${ROW_LABELS[r]} ${COL_LABELS[c]}`\n            const selected = r === row && c === col\n            return (\n              <button\n                key={`${r}-${c}`}\n                type=\"button\"\n                aria-label={label}\n                aria-pressed={selected}\n                onClick={() => pick(r, c)}\n                className={cn(\n                  \"size-12 rounded border font-mono text-[10px]\",\n                  selected\n                    ? \"border-primary bg-primary/15 text-foreground\"\n                    : \"border-input bg-background text-muted-foreground hover:bg-muted/50\",\n                )}\n              >\n                {r === 1 && c === 1 ? \"•\" : \"\"}\n              </button>\n            )\n          }),\n        )}\n      </div>\n\n      <AnchorPreview value={live} />\n    </div>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/anchor-position-editor/position-area-grid.tsx"
    },
    {
      "path": "src/components/ui/anchor-position-editor/anchor-expr-fields.tsx",
      "content": "\"use client\"\n\nimport { Input } from \"@/components/ui/input\"\nimport { UnitInput } from \"@/components/ui/unit-input\"\nimport { cn } from \"@/lib/utils\"\nimport { anchorSides, anchorSizes } from \"./anchor-position-editor.helpers\"\nimport type { AnchorExpr } from \"./anchor-position-editor.types\"\nimport { MiniSelect } from \"./mini-select\"\n\n// ---------------------------------------------------------------------------\n// AnchorExprFields (public) — the `anchor()` / `anchor-size()` editor: an\n// optional `--name` ident input, the function `<select>` (anchor / anchor-size),\n// the side/size `<select>` whose options swap with the function, and an\n// optional `<length-percentage>` fallback driven by `unit-input`. The container\n// owns the `AnchorExpr`; this is presentational.\n// ---------------------------------------------------------------------------\n\n/** A length-only default + the supported fallback unit (px). */\nconst FALLBACK_UNIT = \"px\"\n\nfunction snapSide(fn: AnchorExpr[\"fn\"], current: string): string {\n  const options = fn === \"anchor\" ? anchorSides() : anchorSizes()\n  return options.includes(current) ? current : options[0]\n}\n\nexport interface AnchorExprFieldsProps {\n  expr: AnchorExpr\n  onChange: (next: AnchorExpr) => void\n  className?: string\n}\n\nexport function AnchorExprFields({\n  expr,\n  onChange,\n  className,\n}: AnchorExprFieldsProps) {\n  const sideOptions = expr.fn === \"anchor\" ? anchorSides() : anchorSizes()\n  const hasFallback = expr.fallback !== undefined && expr.fallback !== \"\"\n\n  return (\n    <div className={cn(\"flex flex-wrap items-center gap-2\", className)}>\n      <MiniSelect\n        aria-label=\"anchor function\"\n        value={expr.fn}\n        onValueChange={(v) => {\n          const fn = v as AnchorExpr[\"fn\"]\n          onChange({ ...expr, fn, side: snapSide(fn, expr.side) })\n        }}\n      >\n        <option value=\"anchor\">anchor</option>\n        <option value=\"anchor-size\">anchor-size</option>\n      </MiniSelect>\n\n      <Input\n        aria-label=\"anchor name (optional dashed-ident)\"\n        value={expr.name ?? \"\"}\n        spellCheck={false}\n        autoComplete=\"off\"\n        placeholder=\"--name (optional)\"\n        onChange={(e) => {\n          const name = e.target.value\n          onChange({ ...expr, name: name === \"\" ? undefined : name })\n        }}\n        className=\"h-8 w-[140px] font-mono text-xs\"\n      />\n\n      <MiniSelect\n        aria-label={expr.fn === \"anchor\" ? \"anchor side\" : \"anchor size\"}\n        value={sideOptions.includes(expr.side) ? expr.side : sideOptions[0]}\n        onValueChange={(v) => onChange({ ...expr, side: v })}\n      >\n        {sideOptions.map((s) => (\n          <option key={s} value={s}>\n            {s}\n          </option>\n        ))}\n      </MiniSelect>\n\n      <label className=\"flex h-8 items-center gap-1.5 rounded-md border border-input px-2 font-mono text-muted-foreground text-xs\">\n        <input\n          type=\"checkbox\"\n          aria-label=\"add a fallback length\"\n          checked={hasFallback}\n          onChange={(e) =>\n            onChange({\n              ...expr,\n              fallback: e.target.checked ? `0${FALLBACK_UNIT}` : undefined,\n            })\n          }\n          className=\"size-3.5\"\n        />\n        fallback\n      </label>\n\n      {hasFallback && (\n        <UnitInput\n          aria-label=\"fallback length\"\n          unit={FALLBACK_UNIT}\n          value={expr.fallback ?? `0${FALLBACK_UNIT}`}\n          onChange={(next) => onChange({ ...expr, fallback: next })}\n          className=\"w-[120px]\"\n        />\n      )}\n    </div>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/anchor-position-editor/anchor-expr-fields.tsx"
    },
    {
      "path": "src/components/ui/anchor-position-editor/try-fallback-chain.tsx",
      "content": "\"use client\"\n\nimport { cn } from \"@/lib/utils\"\nimport { formatPositionTry, tryTactics } from \"./anchor-position-editor.helpers\"\nimport type { TryFallback } from \"./anchor-position-editor.types\"\nimport { MiniSelect } from \"./mini-select\"\n\n// ---------------------------------------------------------------------------\n// TryFallbackChain (public) — a reorderable list of `position-try-fallbacks`\n// chips. Each chip is `none`, a `<position-area>` (free text), or the\n// `<dashed-ident> || <try-tactic>` arm (an ident input + a tactic select).\n// Reorder via up/down buttons — no drag-and-drop dependency (A11). Add appends\n// a default tactic fallback; remove drops the chip. The container owns the\n// array; this edits it immutably.\n// ---------------------------------------------------------------------------\n\nconst KINDS: readonly TryFallback[\"kind\"][] = [\"tactics\", \"area\", \"none\"]\n\nfunction defaultFallback(): TryFallback {\n  return { kind: \"tactics\", idents: [], tactics: [\"flip-block\"] }\n}\n\nexport interface TryFallbackChainProps {\n  fallbacks: TryFallback[]\n  onChange: (next: TryFallback[]) => void\n  className?: string\n}\n\nexport function TryFallbackChain({\n  fallbacks,\n  onChange,\n  className,\n}: TryFallbackChainProps) {\n  const updateAt = (index: number, f: TryFallback) =>\n    onChange(fallbacks.map((it, i) => (i === index ? f : it)))\n  const removeAt = (index: number) =>\n    onChange(fallbacks.filter((_, i) => i !== index))\n  const move = (index: number, dir: -1 | 1) => {\n    const target = index + dir\n    if (target < 0 || target >= fallbacks.length) return\n    const next = [...fallbacks]\n    ;[next[index], next[target]] = [next[target], next[index]]\n    onChange(next)\n  }\n  const add = () => onChange([...fallbacks, defaultFallback()])\n\n  return (\n    <div className={cn(\"space-y-2\", className)}>\n      <ul className=\"space-y-2\">\n        {fallbacks.map((f, i) => (\n          <li\n            // biome-ignore lint/suspicious/noArrayIndexKey: chips are positional, reordered only by move/remove\n            key={`fallback-${i}`}\n            aria-label={`fallback ${i + 1}`}\n            className=\"flex flex-wrap items-center gap-1.5 rounded-md border border-input p-1.5\"\n          >\n            <MiniSelect\n              aria-label={`kind of fallback ${i + 1}`}\n              value={f.kind}\n              onValueChange={(v) =>\n                updateAt(i, snapKind(v as TryFallback[\"kind\"], f))\n              }\n            >\n              {KINDS.map((k) => (\n                <option key={k} value={k}>\n                  {k}\n                </option>\n              ))}\n            </MiniSelect>\n\n            <FallbackBody\n              index={i}\n              fallback={f}\n              onChange={(next) => updateAt(i, next)}\n            />\n\n            <div className=\"ml-auto flex items-center gap-1\">\n              <button\n                type=\"button\"\n                aria-label={`Move fallback ${i + 1} up`}\n                onClick={() => move(i, -1)}\n                disabled={i === 0}\n                className=\"h-6 w-6 rounded border font-mono text-xs hover:bg-muted/50 disabled:opacity-40\"\n              >\n                ↑\n              </button>\n              <button\n                type=\"button\"\n                aria-label={`Move fallback ${i + 1} down`}\n                onClick={() => move(i, 1)}\n                disabled={i === fallbacks.length - 1}\n                className=\"h-6 w-6 rounded border font-mono text-xs hover:bg-muted/50 disabled:opacity-40\"\n              >\n                ↓\n              </button>\n              <button\n                type=\"button\"\n                aria-label={`Remove fallback ${i + 1}`}\n                onClick={() => removeAt(i)}\n                className=\"h-6 w-6 rounded border font-mono text-xs hover:bg-muted/50\"\n              >\n                ×\n              </button>\n            </div>\n          </li>\n        ))}\n      </ul>\n\n      <button\n        type=\"button\"\n        onClick={add}\n        aria-label=\"Add a fallback\"\n        className=\"h-9 w-full rounded-md border border-dashed bg-background px-2 font-mono text-muted-foreground text-xs hover:text-foreground\"\n      >\n        + add fallback\n      </button>\n\n      <code className=\"block overflow-x-auto rounded bg-muted/50 px-2 py-1.5 font-mono text-foreground text-xs\">\n        {formatPositionTry(fallbacks) || \" \"}\n      </code>\n    </div>\n  )\n}\n\n/** Re-seed a fallback's body when its kind changes. */\nfunction snapKind(kind: TryFallback[\"kind\"], prev: TryFallback): TryFallback {\n  if (kind === \"none\") return { kind: \"none\" }\n  if (kind === \"area\") return { kind: \"area\", area: prev.area ?? \"top\" }\n  return {\n    kind: \"tactics\",\n    idents: prev.idents ?? [],\n    tactics: prev.tactics ?? [\"flip-block\"],\n  }\n}\n\n// ---------------------------------------------------------------------------\n// FallbackBody — the per-kind editor body.\n// ---------------------------------------------------------------------------\n\nfunction FallbackBody({\n  index,\n  fallback,\n  onChange,\n}: {\n  index: number\n  fallback: TryFallback\n  onChange: (next: TryFallback) => void\n}) {\n  if (fallback.kind === \"none\") {\n    return <span className=\"font-mono text-muted-foreground text-xs\">none</span>\n  }\n\n  if (fallback.kind === \"area\") {\n    return (\n      <input\n        aria-label={`position-area for fallback ${index + 1}`}\n        value={fallback.area ?? \"\"}\n        spellCheck={false}\n        autoComplete=\"off\"\n        placeholder=\"top left\"\n        onChange={(e) => onChange({ ...fallback, area: e.target.value })}\n        className=\"h-8 w-[140px] rounded-md border border-input bg-background px-2 font-mono text-xs\"\n      />\n    )\n  }\n\n  // tactics arm — an ident input + a tactic select\n  const idents = fallback.idents ?? []\n  const tactics = fallback.tactics ?? []\n  return (\n    <>\n      <input\n        aria-label={`dashed-ident for fallback ${index + 1}`}\n        value={idents[0] ?? \"\"}\n        spellCheck={false}\n        autoComplete=\"off\"\n        placeholder=\"--name\"\n        onChange={(e) => {\n          const v = e.target.value.trim()\n          onChange({ ...fallback, idents: v === \"\" ? [] : [v] })\n        }}\n        className=\"h-8 w-[120px] rounded-md border border-input bg-background px-2 font-mono text-xs\"\n      />\n      <MiniSelect\n        aria-label={`tactic for fallback ${index + 1}`}\n        value={tactics[0] ?? \"\"}\n        onValueChange={(v) =>\n          onChange({ ...fallback, tactics: v === \"\" ? [] : [v] })\n        }\n      >\n        <option value=\"\">(no tactic)</option>\n        {tryTactics().map((t) => (\n          <option key={t} value={t}>\n            {t}\n          </option>\n        ))}\n      </MiniSelect>\n    </>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/anchor-position-editor/try-fallback-chain.tsx"
    },
    {
      "path": "src/components/ui/anchor-position-editor/anchor-preview.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useState } from \"react\"\nimport { cn } from \"@/lib/utils\"\n\n// ---------------------------------------------------------------------------\n// AnchorPreview (public) — the live mock-anchor + positioned-box preview for\n// position-area mode. Guards on `CSS.supports(\"position-area: …\")`: in a\n// supporting browser a small box snaps to the chosen area around a mock anchor;\n// elsewhere (jsdom, older browsers) it degrades to a static diagram with a\n// support note (mirrors `if-function`'s degrade path). The preview carries no\n// injection surface — it sets `position-area` from a vetted keyword string.\n// ---------------------------------------------------------------------------\n\nconst SUPPORT_FEATURE = \"position-area: center\"\n\nfunction detectSupport(): boolean {\n  if (typeof CSS === \"undefined\" || typeof CSS.supports !== \"function\") {\n    return false\n  }\n  try {\n    return CSS.supports(SUPPORT_FEATURE)\n  } catch {\n    return false\n  }\n}\n\nexport interface AnchorPreviewProps {\n  /** The `position-area` value to visualize. */\n  value: string\n  className?: string\n}\n\nexport function AnchorPreview({ value, className }: AnchorPreviewProps) {\n  // Detect once on mount (post-hydration) so SSR + jsdom take the static path.\n  const [supported, setSupported] = useState(false)\n  useEffect(() => {\n    setSupported(detectSupport())\n  }, [])\n\n  return (\n    <div className={cn(\"space-y-2 rounded-lg border p-3\", className)}>\n      <div className=\"flex items-center justify-between\">\n        <span className=\"text-muted-foreground text-xs\">preview</span>\n        {supported ? (\n          <span className=\"rounded bg-emerald-500/15 px-2 py-0.5 font-mono text-[10px] text-emerald-400\">\n            anchor positioning ✓\n          </span>\n        ) : (\n          <span className=\"rounded bg-muted px-2 py-0.5 font-mono text-[10px] text-muted-foreground\">\n            not supported\n          </span>\n        )}\n      </div>\n\n      {supported ? (\n        <div\n          role=\"img\"\n          aria-label={`A box placed at ${value} around a mock anchor`}\n          className=\"relative grid h-28 place-items-center rounded-md bg-muted/30\"\n        >\n          {/* The mock anchor — registers an anchor name the box references.\n              `anchorName` / `positionAnchor` / `positionArea` are camelCased\n              (React serializes them to kebab-case CSS) and cast because they\n              are not yet in React's CSSProperties. */}\n          <div\n            style={{ anchorName: \"--preview-anchor\" } as React.CSSProperties}\n            className=\"size-10 rounded border-2 border-primary/50 border-dashed bg-primary/5\"\n          />\n          {/* The positioned box — snaps to the chosen position-area. */}\n          <div\n            style={\n              {\n                position: \"absolute\",\n                positionAnchor: \"--preview-anchor\",\n                positionArea: value || \"center\",\n              } as React.CSSProperties\n            }\n            className=\"size-5 rounded bg-primary\"\n          />\n        </div>\n      ) : (\n        <StaticDiagram value={value} />\n      )}\n\n      <p className=\"text-[10px] text-muted-foreground/70 leading-relaxed\">\n        {supported ? (\n          <>\n            Live placement via <code className=\"font-mono\">position-area</code>{\" \"}\n            around an <code className=\"font-mono\">anchor-name</code>. Resize to\n            see it track.\n          </>\n        ) : (\n          <>\n            CSS anchor positioning is unavailable here — showing a static\n            diagram. The produced value still copies and works in a supporting\n            browser.\n          </>\n        )}\n      </p>\n    </div>\n  )\n}\n\n// ---------------------------------------------------------------------------\n// StaticDiagram — the degraded, support-free placement sketch.\n// ---------------------------------------------------------------------------\n\nfunction StaticDiagram({ value }: { value: string }) {\n  return (\n    <div\n      role=\"img\"\n      aria-label={`Static diagram: a box at ${value} relative to an anchor`}\n      className=\"grid h-28 place-items-center rounded-md bg-muted/30\"\n    >\n      <div className=\"relative size-16\">\n        <div className=\"absolute inset-0 rounded border-2 border-primary/50 border-dashed bg-primary/5\" />\n        <div className=\"absolute top-0 left-0 size-4 rounded bg-primary\" />\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/anchor-position-editor/anchor-preview.tsx"
    }
  ],
  "type": "registry:ui"
}