{"$schema":"https:\/\/ui.particle.academy\/schema\/registry-item.json","name":"json-editor","type":"registry:ui","title":"JsonEditor","description":"Key\/value editing for nested JSON, where a keyMap imposes a declared data type on chosen paths \u2014 driving both how a value displays and which input edits it.","package":"react-fancy","dependencies":[],"registryDependencies":["inputs","button","callout","text","badge","color-picker"],"files":[{"path":"components\/fancy\/json-editor\/JsonEditor.context.ts","content":"import { createContext, useContext } from \"react\";\nimport type { FieldMode } from \"..\/inputs\/inputs.types\";\nimport type { Size } from \"..\/..\/utils\/types\";\nimport type {\n  JsonEditorNode,\n  JsonEditorPendingEdit,\n  JsonFieldType,\n  JsonValue,\n} from \".\/JsonEditor.types\";\n\n\/**\n * Everything a row needs that is not the row itself.\n *\n * Same shape as `Table`'s context and for the same reason: a row is rendered\n * once per key in a document that can be hundreds deep, and threading a dozen\n * props through the recursion makes every signature change a rewrite.\n *\/\nexport interface JsonEditorContextValue {\n  mode: FieldMode;\n  size: Size;\n  readOnly: boolean;\n  allowAdd: boolean;\n  allowRemove: boolean;\n  allowRename: boolean;\n  allowReorder: boolean;\n  pendingMode: boolean;\n  idPrefix: string;\n\n  isExpanded: (dotted: string) => boolean;\n  toggleExpanded: (dotted: string) => void;\n\n  setValueAt: (node: JsonEditorNode, value: JsonValue) => void;\n  removeAt: (node: JsonEditorNode) => void;\n  renameAt: (node: JsonEditorNode, key: string) => void;\n  moveAt: (node: JsonEditorNode, to: number) => void;\n  insertInto: (dotted: string, key: string | undefined, type: JsonFieldType) => void;\n\n  \/** Which container's add form is open, if any. Transient UI state, never model state. *\/\n  addingAt: string | null;\n  setAddingAt: (dotted: string | null) => void;\n\n  pendingFor: (dotted: string) => JsonEditorPendingEdit[];\n  acceptPending: (id: string) => void;\n  rejectPending: (id: string) => void;\n}\n\nexport const JsonEditorContext = createContext<JsonEditorContextValue | null>(null);\n\nexport function useJsonEditorContext(): JsonEditorContextValue {\n  const context = useContext(JsonEditorContext);\n  if (!context) {\n    throw new Error(\"JsonEditor rows must be rendered inside a <JsonEditor>.\");\n  }\n  return context;\n}\n","type":"registry:ui","target":"components\/fancy\/json-editor\/JsonEditor.context.ts"},{"path":"components\/fancy\/json-editor\/JsonEditor.keymap.ts","content":"import { resolveOption } from \"..\/inputs\/inputs.utils\";\nimport {\n  getAtPath,\n  isJsonArray,\n  isJsonObject,\n  parsePath,\n  pathToString,\n} from \".\/JsonEditor.paths\";\nimport { JSON_FIELD_TYPES } from \".\/JsonEditor.types\";\nimport type {\n  JsonEditorIssue,\n  JsonFieldType,\n  JsonKeyMapEntry,\n  JsonKeyRule,\n  JsonPath,\n  JsonValue,\n  ParsedKeyMap,\n} from \".\/JsonEditor.types\";\n\n\/\/ ---------------------------------------------------------------------------\n\/\/ The keyMap\n\/\/\n\/\/ Shape: a FLAT object whose keys are dotted path patterns and whose values are\n\/\/ either a bare type name or a rule object.\n\/\/\n\/\/   { \"user.age\": \"number\", \"orders.*.total\": \"number\", \"tags.*\": \"string\" }\n\/\/\n\/\/ Why flat paths rather than a nested mirror of the data:\n\/\/\n\/\/  - A nested mirror is ambiguous the moment a key needs a type AND typed\n\/\/    children \u2014 `{\"user\": {\"age\": \"number\"}}` could mean either, and resolving\n\/\/    it needs a magic key (`$type`) that a flat map does not.\n\/\/  - Arrays force a wildcard into a nested mirror anyway, so the mirror buys\n\/\/    nothing there and costs a level of nesting per segment.\n\/\/  - One rule per line is greppable, diffable and reviewable, which matters\n\/\/    when the map arrives as a string from an agent or a config column.\n\/\/  - It is sparse by construction: you declare the keys you care about, and\n\/\/    everything else is inferred from the value.\n\/\/\n\/\/ A `*` segment matches exactly one key or index. There is deliberately no\n\/\/ `**`: \"any depth\" makes two patterns overlap in ways that are hard to reason\n\/\/ about, and every case we had for it was really \"this array's elements\".\n\/\/ ---------------------------------------------------------------------------\n\nconst TYPE_NAMES = new Set<string>(JSON_FIELD_TYPES);\n\nfunction isTypeName(value: unknown): value is JsonFieldType {\n  return typeof value === \"string\" && TYPE_NAMES.has(value);\n}\n\nfunction issue(kind: JsonEditorIssue[\"kind\"], path: string, message: string): JsonEditorIssue {\n  return { kind, path, message };\n}\n\n\/**\n * Compile a `keyMap` JSON **string**.\n *\n * Never throws. Two failure modes, deliberately distinct:\n *\n *  - The string itself is unusable (not JSON, or JSON that is not an object) \u2014\n *    `ok: false`, no rules, one `keymap` issue. The caller must show this: an\n *    editor that quietly runs untyped is worse than one that refuses, because\n *    the person editing believes the constraint is in force.\n *  - One entry is malformed \u2014 `ok: true`, that entry dropped, one `rule` issue.\n *    A typo in `orders.*.totl` must not disable the other forty rules.\n *\/\nexport function parseKeyMap(source?: string | null): ParsedKeyMap {\n  if (source === undefined || source === null || source.trim() === \"\") {\n    return { ok: true, rules: [], issues: [] };\n  }\n\n  let parsed: unknown;\n  try {\n    parsed = JSON.parse(source);\n  } catch (error) {\n    return {\n      ok: false,\n      rules: [],\n      issues: [\n        issue(\n          \"keymap\",\n          \"\",\n          `keyMap is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,\n        ),\n      ],\n    };\n  }\n\n  if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n    return {\n      ok: false,\n      rules: [],\n      issues: [\n        issue(\n          \"keymap\",\n          \"\",\n          `keyMap must be a JSON object of path \u2192 type, got ${describeValue(parsed as JsonValue)}.`,\n        ),\n      ],\n    };\n  }\n\n  const rules: JsonKeyMapEntry[] = [];\n  const issues: JsonEditorIssue[] = [];\n  let order = 0;\n\n  for (const [key, raw] of Object.entries(parsed as Record<string, unknown>)) {\n    const rule = normalizeRule(key, raw, issues);\n    if (!rule) continue;\n\n    const pattern = parsePath(key);\n    rules.push({\n      pattern,\n      rule,\n      source: key,\n      literals: pattern.reduce((n, segment) => n + (segment === \"*\" ? 0 : 1), 0),\n      order: order++,\n    });\n  }\n\n  return { ok: true, rules, issues };\n}\n\nfunction normalizeRule(\n  key: string,\n  raw: unknown,\n  issues: JsonEditorIssue[],\n): JsonKeyRule | undefined {\n  let candidate: Record<string, unknown>;\n\n  if (typeof raw === \"string\") {\n    candidate = { type: raw };\n  } else if (typeof raw === \"object\" && raw !== null && !Array.isArray(raw)) {\n    candidate = raw as Record<string, unknown>;\n  } else {\n    issues.push(\n      issue(\n        \"rule\",\n        key,\n        `Expected a type name or a rule object, got ${describeValue(raw as JsonValue)}.`,\n      ),\n    );\n    return undefined;\n  }\n\n  if (!isTypeName(candidate.type)) {\n    issues.push(\n      issue(\n        \"rule\",\n        key,\n        `Unknown type ${JSON.stringify(candidate.type)}. Known types: ${JSON_FIELD_TYPES.join(\", \")}.`,\n      ),\n    );\n    return undefined;\n  }\n\n  if (candidate.options !== undefined && !Array.isArray(candidate.options)) {\n    issues.push(issue(\"rule\", key, \"`options` must be an array.\"));\n    return undefined;\n  }\n\n  \/\/ An enum with nothing to choose from cannot type anything, and would render\n  \/\/ an empty select that looks like a loading state.\n  if (candidate.type === \"enum\" && (!candidate.options || candidate.options.length === 0)) {\n    issues.push(issue(\"rule\", key, \"`enum` requires a non-empty `options` array.\"));\n    return undefined;\n  }\n\n  return candidate as unknown as JsonKeyRule;\n}\n\n\/**\n * The rule governing `segments`, or `undefined` when the map is silent.\n *\n * Specificity: the pattern with the most literal (non-`*`) segments wins, and\n * at equal specificity the one declared LAST wins \u2014 so a general\n * `orders.*.total` can be overridden by a specific `orders.0.total` placed\n * anywhere in the map.\n *\/\nexport function resolveKeyRule(\n  rules: JsonKeyMapEntry[],\n  segments: JsonPath,\n): JsonKeyRule | undefined {\n  let best: JsonKeyMapEntry | undefined;\n\n  for (const entry of rules) {\n    if (!matches(entry.pattern, segments)) continue;\n    if (\n      !best ||\n      entry.literals > best.literals ||\n      (entry.literals === best.literals && entry.order > best.order)\n    ) {\n      best = entry;\n    }\n  }\n\n  return best?.rule;\n}\n\nfunction matches(pattern: JsonPath, segments: JsonPath): boolean {\n  if (pattern.length !== segments.length) return false;\n  return pattern.every((p, i) => p === \"*\" || p === segments[i]);\n}\n\n\/** What a value IS, in the vocabulary the issue messages use. *\/\nexport function describeValue(value: JsonValue | undefined): string {\n  if (value === undefined) return \"undefined\";\n  if (value === null) return \"null\";\n  if (Array.isArray(value)) return \"array\";\n  return typeof value;\n}\n\n\/** The type a value carries on its own, when nothing declares one for it. *\/\nexport function inferType(value: JsonValue | undefined): JsonFieldType {\n  if (Array.isArray(value)) return \"array\";\n  if (value === null || value === undefined) {\n    \/\/ `null` carries no type information at all, so the raw editor is the only\n    \/\/ choice that neither invents a type nor drops the value.\n    return \"json\";\n  }\n  if (typeof value === \"object\") return \"object\";\n  if (typeof value === \"boolean\") return \"boolean\";\n  if (typeof value === \"number\") return \"number\";\n  return \"string\";\n}\n\nfunction enumValues(rule: JsonKeyRule): (string | number | boolean)[] {\n  return (rule.options ?? []).map((option) => resolveOption(option).value as string);\n}\n\n\/**\n * Does the value satisfy the declared type?\n *\n * Deliberately strict and deliberately non-coercing. `\"36\"` is NOT a number\n * here \u2014 the whole reason a `keyMap` exists is to make that visible rather than\n * to paper over it.\n *\/\nexport function typeMatches(rule: JsonKeyRule, value: JsonValue | undefined): boolean {\n  switch (rule.type) {\n    case \"string\":\n    case \"text\":\n    case \"url\":\n    case \"email\":\n    case \"secret\":\n    case \"color\":\n      return typeof value === \"string\";\n    case \"number\":\n      return typeof value === \"number\" && Number.isFinite(value);\n    case \"integer\":\n      return typeof value === \"number\" && Number.isInteger(value);\n    case \"boolean\":\n      return typeof value === \"boolean\";\n    case \"date\":\n    case \"datetime\":\n      return typeof value === \"string\" && value !== \"\" && !Number.isNaN(Date.parse(value));\n    case \"enum\":\n      return (\n        (typeof value === \"string\" || typeof value === \"number\" || typeof value === \"boolean\") &&\n        enumValues(rule).some((option) => String(option) === String(value))\n      );\n    case \"object\":\n      return isJsonObject(value);\n    case \"array\":\n      return isJsonArray(value);\n    case \"json\":\n      \/\/ The escape hatch matches anything EXCEPT text that does not parse \u2014\n      \/\/ which is exactly the state the raw editor leaves behind when someone\n      \/\/ saves a half-written document.\n      if (typeof value !== \"string\") return true;\n      try {\n        JSON.parse(value);\n        return true;\n      } catch {\n        return false;\n      }\n  }\n}\n\nfunction conflictMessage(rule: JsonKeyRule, value: JsonValue | undefined): string {\n  if (rule.type === \"enum\") {\n    return `${JSON.stringify(value)} is not one of the declared options (${enumValues(rule)\n      .map((option) => JSON.stringify(option))\n      .join(\", \")}).`;\n  }\n  if (rule.type === \"json\") {\n    return \"Raw text is not valid JSON.\";\n  }\n  if (rule.type === \"date\" || rule.type === \"datetime\") {\n    return `Declared ${rule.type}, got ${describeValue(value)} that is not a parseable date.`;\n  }\n  return `Declared ${rule.type}, got ${describeValue(value)}.`;\n}\n\n\/**\n * Every place the document contradicts its `keyMap`.\n *\n * Walks the value (so a conflict is found at any depth, arrays included), then\n * checks `required` on the wildcard-free patterns \u2014 a wildcard cannot say \"this\n * must exist\" about a key nobody has named.\n *\/\nexport function findJsonConflicts(value: JsonValue, rules: JsonKeyMapEntry[]): JsonEditorIssue[] {\n  const issues: JsonEditorIssue[] = [];\n  if (rules.length === 0) return issues;\n\n  const walk = (node: JsonValue, segments: JsonPath) => {\n    if (segments.length > 0) {\n      const rule = resolveKeyRule(rules, segments);\n      if (rule && !typeMatches(rule, node)) {\n        issues.push({\n          kind: \"type\",\n          path: pathToString(segments),\n          message: conflictMessage(rule, node),\n          expected: rule.type,\n          actual: describeValue(node),\n        });\n      }\n    }\n\n    if (isJsonArray(node)) {\n      node.forEach((child, index) => walk(child, [...segments, String(index)]));\n    } else if (isJsonObject(node)) {\n      for (const [key, child] of Object.entries(node)) walk(child, [...segments, key]);\n    }\n  };\n\n  walk(value, []);\n\n  for (const entry of rules) {\n    if (!entry.rule.required) continue;\n    if (entry.pattern.includes(\"*\")) continue;\n    if (getAtPath(value, entry.pattern) !== undefined) continue;\n    issues.push({\n      kind: \"type\",\n      path: entry.source,\n      message: `Required key is missing (declared ${entry.rule.type}).`,\n      expected: entry.rule.type,\n      actual: \"undefined\",\n    });\n  }\n\n  return issues;\n}\n\n\/** The value a newly-added key starts as, so a new row is never `undefined`. *\/\nexport function seedValue(type: JsonFieldType, rule?: JsonKeyRule): JsonValue {\n  switch (type) {\n    case \"number\":\n    case \"integer\":\n      return 0;\n    case \"boolean\":\n      return false;\n    case \"object\":\n      return {};\n    case \"array\":\n      return [];\n    case \"json\":\n      return null;\n    case \"enum\":\n      return rule?.options?.length ? (resolveOption(rule.options[0]!).value as string) : \"\";\n    default:\n      return \"\";\n  }\n}\n","type":"registry:ui","target":"components\/fancy\/json-editor\/JsonEditor.keymap.ts"},{"path":"components\/fancy\/json-editor\/JsonEditor.paths.ts","content":"import type {\n  JsonArray,\n  JsonEditorEdit,\n  JsonObject,\n  JsonPath,\n  JsonValue,\n} from \".\/JsonEditor.types\";\n\n\/\/ ---------------------------------------------------------------------------\n\/\/ Paths\n\/\/\n\/\/ The wire form of a path is a dotted string \u2014 `user.address.city`,\n\/\/ `orders.0.total` \u2014 because that is what fits in a `data-*` attribute, a\n\/\/ keyMap key, an MCP tool argument, and a log line. `.` separates segments and\n\/\/ `\\` escapes, so a key that genuinely contains a dot is still addressable\n\/\/ (`meta.a\\.b` is `[\"meta\", \"a.b\"]`, not three segments).\n\/\/ ---------------------------------------------------------------------------\n\nexport function isJsonObject(value: JsonValue | undefined): value is JsonObject {\n  return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nexport function isJsonArray(value: JsonValue | undefined): value is JsonArray {\n  return Array.isArray(value);\n}\n\nexport function isContainerValue(value: JsonValue | undefined): boolean {\n  return isJsonObject(value) || isJsonArray(value);\n}\n\n\/** Split a dotted path into segments, honouring `\\.` and `\\\\`. *\/\nexport function parsePath(path: string): JsonPath {\n  if (!path) return [];\n\n  const segments: string[] = [];\n  let current = \"\";\n  let escaped = false;\n\n  for (const char of path) {\n    if (escaped) {\n      current += char;\n      escaped = false;\n      continue;\n    }\n    if (char === \"\\\\\") {\n      escaped = true;\n      continue;\n    }\n    if (char === \".\") {\n      segments.push(current);\n      current = \"\";\n      continue;\n    }\n    current += char;\n  }\n  \/\/ A trailing lone backslash is a typo, not a separator \u2014 keep it literal\n  \/\/ rather than throwing on a half-typed path.\n  if (escaped) current += \"\\\\\";\n  segments.push(current);\n\n  return segments;\n}\n\n\/** The inverse of {@link parsePath}. *\/\nexport function pathToString(segments: JsonPath): string {\n  return segments\n    .map((segment) => segment.replace(\/\\\\\/g, \"\\\\\\\\\").replace(\/\\.\/g, \"\\\\.\"))\n    .join(\".\");\n}\n\nexport function getAtPath(value: JsonValue, segments: JsonPath): JsonValue | undefined {\n  let current: JsonValue | undefined = value;\n\n  for (const segment of segments) {\n    if (isJsonArray(current)) {\n      const index = Number(segment);\n      if (!Number.isInteger(index)) return undefined;\n      current = current[index];\n    } else if (isJsonObject(current)) {\n      if (!Object.prototype.hasOwnProperty.call(current, segment)) return undefined;\n      current = current[segment];\n    } else {\n      return undefined;\n    }\n    if (current === undefined) return undefined;\n  }\n\n  return current;\n}\n\n\/**\n * Rebuild `value` with `updater` applied to the container at `segments`,\n * cloning only the containers along that path. Returns the original reference\n * when the path does not resolve, so a no-op edit is detectable by identity.\n *\/\nfunction updateContainer(\n  value: JsonValue,\n  segments: JsonPath,\n  updater: (container: JsonObject | JsonArray) => JsonObject | JsonArray | undefined,\n): JsonValue {\n  if (segments.length === 0) {\n    if (!isContainerValue(value)) return value;\n    const next = updater(value as JsonObject | JsonArray);\n    return next === undefined ? value : next;\n  }\n\n  const [head, ...rest] = segments as [string, ...string[]];\n\n  if (isJsonArray(value)) {\n    const index = Number(head);\n    if (!Number.isInteger(index) || index < 0 || index >= value.length) return value;\n    const child = updateContainer(value[index]!, rest, updater);\n    if (child === value[index]) return value;\n    const clone = value.slice();\n    clone[index] = child;\n    return clone;\n  }\n\n  if (isJsonObject(value)) {\n    if (!Object.prototype.hasOwnProperty.call(value, head)) return value;\n    const child = updateContainer(value[head]!, rest, updater);\n    if (child === value[head]) return value;\n    return { ...value, [head]: child };\n  }\n\n  return value;\n}\n\n\/**\n * Apply one {@link JsonEditorEdit} and return a NEW document.\n *\n * Pure and total: an edit that cannot be applied \u2014 a path that does not exist,\n * a rename onto an occupied key, an insert without a key into an object \u2014\n * returns the input unchanged rather than throwing or half-applying. The\n * component checks those cases first and raises an issue; this function is the\n * backstop for the same op arriving from a bridge, where there is no UI to warn.\n *\/\nexport function applyJsonEdit(value: JsonValue, edit: JsonEditorEdit): JsonValue {\n  const segments = parsePath(edit.path);\n\n  switch (edit.op) {\n    case \"set\": {\n      \/\/ An empty path is the document itself.\n      if (segments.length === 0) return edit.value;\n\n      const parent = segments.slice(0, -1);\n      const key = segments[segments.length - 1]!;\n\n      return updateContainer(value, parent, (container) => {\n        if (isJsonArray(container)) {\n          const index = Number(key);\n          if (!Number.isInteger(index) || index < 0 || index >= container.length) return undefined;\n          const clone = container.slice();\n          clone[index] = edit.value;\n          return clone;\n        }\n        return { ...container, [key]: edit.value };\n      });\n    }\n\n    case \"remove\": {\n      if (segments.length === 0) return value;\n\n      const parent = segments.slice(0, -1);\n      const key = segments[segments.length - 1]!;\n\n      return updateContainer(value, parent, (container) => {\n        if (isJsonArray(container)) {\n          const index = Number(key);\n          if (!Number.isInteger(index) || index < 0 || index >= container.length) return undefined;\n          return container.filter((_, i) => i !== index);\n        }\n        if (!Object.prototype.hasOwnProperty.call(container, key)) return undefined;\n        const clone: JsonObject = {};\n        for (const [k, v] of Object.entries(container)) {\n          if (k !== key) clone[k] = v;\n        }\n        return clone;\n      });\n    }\n\n    case \"rename\": {\n      if (segments.length === 0) return value;\n\n      const parent = segments.slice(0, -1);\n      const key = segments[segments.length - 1]!;\n\n      return updateContainer(value, parent, (container) => {\n        \/\/ Array indices are positional, not names \u2014 reorder is `move`.\n        if (!isJsonObject(container)) return undefined;\n        if (!Object.prototype.hasOwnProperty.call(container, key)) return undefined;\n        if (edit.key === key) return undefined;\n        \/\/ Renaming onto a live sibling would silently destroy it.\n        if (Object.prototype.hasOwnProperty.call(container, edit.key)) return undefined;\n\n        \/\/ Rebuilt in order: `delete` + re-add would move the key to the end,\n        \/\/ which reads as the row jumping down the list for no reason.\n        const clone: JsonObject = {};\n        for (const [k, v] of Object.entries(container)) {\n          clone[k === key ? edit.key : k] = v;\n        }\n        return clone;\n      });\n    }\n\n    case \"insert\": {\n      return updateContainer(value, segments, (container) => {\n        if (isJsonArray(container)) return [...container, edit.value];\n        if (edit.key === undefined || edit.key === \"\") return undefined;\n        if (Object.prototype.hasOwnProperty.call(container, edit.key)) return undefined;\n        return { ...container, [edit.key]: edit.value };\n      });\n    }\n\n    case \"move\": {\n      if (segments.length === 0) return value;\n\n      const parent = segments.slice(0, -1);\n      const from = Number(segments[segments.length - 1]);\n\n      return updateContainer(value, parent, (container) => {\n        if (!isJsonArray(container)) return undefined;\n        if (!Number.isInteger(from) || from < 0 || from >= container.length) return undefined;\n        const to = Math.min(Math.max(edit.to, 0), container.length - 1);\n        if (to === from) return undefined;\n        const clone = container.slice();\n        const [moved] = clone.splice(from, 1);\n        clone.splice(to, 0, moved!);\n        return clone;\n      });\n    }\n  }\n}\n\n\/** A one-line summary of an edit, for the pending strip and activity events. *\/\nexport function describeEdit(edit: JsonEditorEdit): string {\n  const at = edit.path === \"\" ? \"the document\" : edit.path;\n  switch (edit.op) {\n    case \"set\":\n      return `set ${at}`;\n    case \"remove\":\n      return `remove ${at}`;\n    case \"rename\":\n      return `rename ${at} to ${edit.key}`;\n    case \"insert\":\n      return edit.key === undefined ? `append to ${at}` : `add ${edit.key} to ${at}`;\n    case \"move\":\n      return `move ${at} to index ${edit.to}`;\n  }\n}\n","type":"registry:ui","target":"components\/fancy\/json-editor\/JsonEditor.paths.ts"},{"path":"components\/fancy\/json-editor\/JsonEditor.tsx","content":"import { forwardRef, useCallback, useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport { cn } from \"..\/..\/utils\/cn\";\nimport { Button } from \"..\/Button\/Button\";\nimport { Callout } from \"..\/Callout\/Callout\";\nimport { Text } from \"..\/Text\/Text\";\nimport { JsonEditorContext } from \".\/JsonEditor.context\";\nimport { JsonEditorAddForm, JsonEditorRow } from \".\/JsonEditorRow\";\nimport { findJsonConflicts, inferType, parseKeyMap, resolveKeyRule, seedValue } from \".\/JsonEditor.keymap\";\nimport {\n  applyJsonEdit,\n  describeEdit,\n  getAtPath,\n  isContainerValue,\n  isJsonArray,\n  isJsonObject,\n  parsePath,\n  pathToString,\n} from \".\/JsonEditor.paths\";\nimport type { JsonEditorContextValue } from \".\/JsonEditor.context\";\nimport type {\n  JsonEditorEdit,\n  JsonEditorIssue,\n  JsonEditorNode,\n  JsonEditorPendingEdit,\n  JsonEditorProps,\n  JsonFieldType,\n  JsonKeyMapEntry,\n  JsonPath,\n  JsonValue,\n} from \".\/JsonEditor.types\";\n\ntype RenderItem =\n  | { kind: \"node\"; node: JsonEditorNode }\n  | {\n      kind: \"add\";\n      path: string;\n      containerKind: \"object\" | \"array\";\n      depth: number;\n      suggestedType?: JsonFieldType;\n    };\n\n\/** Dotted paths of every container in the document \u2014 the \"expand everything\" set. *\/\nfunction collectContainerPaths(value: JsonValue): string[] {\n  const out: string[] = [];\n\n  const walk = (node: JsonValue, segments: JsonPath) => {\n    if (!isContainerValue(node)) return;\n    if (segments.length > 0) out.push(pathToString(segments));\n    if (isJsonArray(node)) {\n      node.forEach((child, index) => walk(child, [...segments, String(index)]));\n    } else if (isJsonObject(node)) {\n      for (const [key, child] of Object.entries(node)) walk(child, [...segments, key]);\n    }\n  };\n\n  walk(value, []);\n  return out;\n}\n\n\/**\n * JsonEditor \u2014 a key\/value editor over arbitrary, arbitrarily-nested JSON, with\n * a caller-supplied `keyMap` imposing a data type on any path.\n *\n * The type does two jobs at once, which is the point of the feature: it decides\n * how a value is RENDERED when you are reading, and which control appears when\n * you are editing. `{\"user.age\": \"number\"}` is not documentation \u2014 it is the\n * reason that row is a numeric field and the reason `\"thirty-six\"` shows up in\n * red instead of quietly becoming `0`.\n *\n * The three design decisions worth knowing before you use it:\n *\n *  - **`keyMap` is a JSON string, and only a string.** It has to survive an MCP\n *    tool argument, a config column and a `data-*` attribute; a live object\n *    survives none of those. See {@link JsonEditorProps.keyMap}.\n *  - **Nothing is coerced and nothing is dropped.** A value that contradicts\n *    its declared type keeps its real value, renders through the raw editor,\n *    and is reported through the issues panel, the `data-issues` count and\n *    `onIssuesChange` \u2014 the same channel a broken `keyMap` uses.\n *  - **It is controlled, all the way down.** The document lives in `value`.\n *    The only state here is which rows are expanded, which add-form is open,\n *    and the half-typed text inside a control that has not committed yet.\n *\n * Every control is a `react-fancy` primitive, so restyling the kit restyles\n * this, and `mode=\"view\"` gets the kit's click-to-edit behaviour for free.\n *\/\nexport const JsonEditor = forwardRef<HTMLDivElement, JsonEditorProps>(function JsonEditor(\n  {\n    value,\n    onChange,\n    keyMap,\n    mode = \"view\",\n    size = \"sm\",\n    readOnly = false,\n    expanded,\n    defaultExpanded,\n    onExpandedChange,\n    showIssues = true,\n    onIssuesChange,\n    pendingMode = false,\n    pending,\n    onPendingChange,\n    onActivity,\n    allowAdd = true,\n    allowRemove = true,\n    allowRename = true,\n    allowReorder = true,\n    rootLabel = \"value\",\n    emptyLabel = \"No keys yet.\",\n    idPrefix,\n    className,\n    ...rest\n  },\n  ref,\n) {\n  const autoId = useId();\n  const prefix = idPrefix ?? autoId;\n  const editSeq = useRef(0);\n\n  \/\/ Parsed ONCE per distinct keyMap string \u2014 not per row, not per render. The\n  \/\/ map is compiled into path patterns with a specificity score, and rows only\n  \/\/ ever do a match against that.\n  const parsedKeyMap = useMemo(() => parseKeyMap(keyMap), [keyMap]);\n  const rules = parsedKeyMap.rules;\n\n  const conflicts = useMemo(() => findJsonConflicts(value, rules), [value, rules]);\n\n  \/\/ Transient complaints about an edit that was refused (duplicate key, empty\n  \/\/ key). They belong beside the type conflicts because from the user's side\n  \/\/ they are the same thing: something they can see is wrong and can fix.\n  const [editIssues, setEditIssues] = useState<JsonEditorIssue[]>([]);\n\n  const issues = useMemo(\n    () => [...parsedKeyMap.issues, ...conflicts, ...editIssues],\n    [parsedKeyMap.issues, conflicts, editIssues],\n  );\n\n  const onIssuesChangeRef = useRef(onIssuesChange);\n  onIssuesChangeRef.current = onIssuesChange;\n  const lastIssueSignature = useRef<string | null>(null);\n\n  useEffect(() => {\n    const signature = JSON.stringify(issues);\n    if (signature === lastIssueSignature.current) return;\n    lastIssueSignature.current = signature;\n    onIssuesChangeRef.current?.(issues);\n  }, [issues]);\n\n  \/\/ \u2500\u2500 expansion \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  \/\/ `null` means \"expanded, everything\" \u2014 the state a caller that named no\n  \/\/ paths is in. Storing it as a sentinel rather than materialising every\n  \/\/ container path keeps a container added later expanded too.\n  const expandedControlled = expanded !== undefined;\n  const [expandedUncontrolled, setExpandedUncontrolled] = useState<string[] | null>(\n    defaultExpanded ?? null,\n  );\n  const expandedList = expandedControlled ? expanded : expandedUncontrolled;\n\n  const isExpanded = useCallback(\n    (dotted: string) => expandedList === null || expandedList.includes(dotted),\n    [expandedList],\n  );\n\n  const toggleExpanded = useCallback(\n    (dotted: string) => {\n      const current = expandedList ?? collectContainerPaths(value);\n      const next = current.includes(dotted)\n        ? current.filter((path) => path !== dotted)\n        : [...current, dotted];\n      if (!expandedControlled) setExpandedUncontrolled(next);\n      onExpandedChange?.(next);\n    },\n    [expandedList, expandedControlled, onExpandedChange, value],\n  );\n\n  \/\/ \u2500\u2500 add form \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  const [addingAt, setAddingAt] = useState<string | null>(null);\n\n  \/\/ \u2500\u2500 committing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  const stagedPending = useMemo(() => pending ?? [], [pending]);\n\n  const commit = useCallback(\n    (edit: JsonEditorEdit) => {\n      setEditIssues([]);\n\n      if (pendingMode) {\n        \/\/ Trust-but-verify: the edit becomes a PROPOSAL. `value` does not move\n        \/\/ and `onChange` does not fire until a human accepts it.\n        const staged: JsonEditorPendingEdit = {\n          ...edit,\n          id: edit.id ?? `${prefix}-edit-${editSeq.current++}`,\n          label: edit.label ?? describeEdit(edit),\n        };\n        onPendingChange?.([...stagedPending, staged]);\n        onActivity?.({ type: \"stage\", edit: staged });\n        return;\n      }\n\n      const next = applyJsonEdit(value, edit);\n      \/\/ `applyJsonEdit` returns the same reference when it refuses an edit, so\n      \/\/ a refusal never reaches the host as a spurious \"changed\" callback.\n      if (next === value) return;\n      onChange?.(next, edit);\n      onActivity?.({ type: \"commit\", edit });\n    },\n    [pendingMode, prefix, stagedPending, onPendingChange, onActivity, value, onChange],\n  );\n\n  const refuse = useCallback((path: string, message: string) => {\n    setEditIssues([{ kind: \"edit\", path, message }]);\n  }, []);\n\n  const setValueAt = useCallback(\n    (node: JsonEditorNode, next: JsonValue) => {\n      commit({ op: \"set\", path: node.dotted, value: next });\n    },\n    [commit],\n  );\n\n  const removeAt = useCallback(\n    (node: JsonEditorNode) => {\n      commit({ op: \"remove\", path: node.dotted });\n    },\n    [commit],\n  );\n\n  const moveAt = useCallback(\n    (node: JsonEditorNode, to: number) => {\n      commit({ op: \"move\", path: node.dotted, to });\n    },\n    [commit],\n  );\n\n  const renameAt = useCallback(\n    (node: JsonEditorNode, key: string) => {\n      if (key === node.key) return;\n      if (key.trim() === \"\") {\n        refuse(node.dotted, \"A key cannot be empty.\");\n        return;\n      }\n      const parent = getAtPath(value, node.path.slice(0, -1));\n      if (isJsonObject(parent) && Object.prototype.hasOwnProperty.call(parent, key)) {\n        refuse(node.dotted, `A key named \"${key}\" already exists here \u2014 rename refused.`);\n        return;\n      }\n      commit({ op: \"rename\", path: node.dotted, key });\n    },\n    [commit, refuse, value],\n  );\n\n  const insertInto = useCallback(\n    (dotted: string, key: string | undefined, type: JsonFieldType) => {\n      const segments = parsePath(dotted);\n      const container = dotted === \"\" ? value : getAtPath(value, segments);\n      const isArray = isJsonArray(container);\n\n      if (!isArray) {\n        if (key === undefined || key.trim() === \"\") {\n          refuse(dotted, \"A new key needs a name.\");\n          return;\n        }\n        if (isJsonObject(container) && Object.prototype.hasOwnProperty.call(container, key)) {\n          refuse(dotted, `A key named \"${key}\" already exists here.`);\n          return;\n        }\n      }\n\n      const rule = resolveKeyRule(rules, [...segments, isArray ? \"*\" : key!]);\n      setAddingAt(null);\n      commit({\n        op: \"insert\",\n        path: dotted,\n        key: isArray ? undefined : key,\n        value: seedValue(type, rule),\n      });\n    },\n    [commit, refuse, rules, value],\n  );\n\n  const pendingFor = useCallback(\n    (dotted: string) => stagedPending.filter((edit) => edit.path === dotted),\n    [stagedPending],\n  );\n\n  const acceptPending = useCallback(\n    (id: string) => {\n      const edit = stagedPending.find((candidate) => candidate.id === id);\n      if (!edit) return;\n      const next = applyJsonEdit(value, edit);\n      onPendingChange?.(stagedPending.filter((candidate) => candidate.id !== id));\n      if (next !== value) {\n        onChange?.(next, edit);\n        onActivity?.({ type: \"accept\", edit });\n      }\n    },\n    [stagedPending, value, onPendingChange, onChange, onActivity],\n  );\n\n  const rejectPending = useCallback(\n    (id: string) => {\n      const edit = stagedPending.find((candidate) => candidate.id === id);\n      onPendingChange?.(stagedPending.filter((candidate) => candidate.id !== id));\n      if (edit) onActivity?.({ type: \"reject\", edit });\n    },\n    [stagedPending, onPendingChange, onActivity],\n  );\n\n  \/\/ \u2500\u2500 rows \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  const items = useMemo(\n    () =>\n      buildItems({\n        value,\n        rules,\n        conflicts,\n        isExpanded,\n        addingAt,\n        rootLabel,\n      }),\n    [value, rules, conflicts, isExpanded, addingAt, rootLabel],\n  );\n\n  const rootIsContainer = isContainerValue(value);\n  const rootKind = isJsonArray(value) ? \"array\" : \"object\";\n  const attachedIds = new Set(\n    items.flatMap((item) => (item.kind === \"node\" ? [item.node.dotted] : [])),\n  );\n  const orphanPending = stagedPending.filter((edit) => !attachedIds.has(edit.path));\n\n  const context = useMemo<JsonEditorContextValue>(\n    () => ({\n      mode: readOnly ? \"view\" : mode,\n      size,\n      readOnly,\n      allowAdd: allowAdd && !readOnly,\n      allowRemove: allowRemove && !readOnly,\n      allowRename: allowRename && !readOnly,\n      allowReorder: allowReorder && !readOnly,\n      pendingMode,\n      idPrefix: prefix,\n      isExpanded,\n      toggleExpanded,\n      setValueAt,\n      removeAt,\n      renameAt,\n      moveAt,\n      insertInto,\n      addingAt,\n      setAddingAt,\n      pendingFor,\n      acceptPending,\n      rejectPending,\n    }),\n    [\n      mode,\n      size,\n      readOnly,\n      allowAdd,\n      allowRemove,\n      allowRename,\n      allowReorder,\n      pendingMode,\n      prefix,\n      isExpanded,\n      toggleExpanded,\n      setValueAt,\n      removeAt,\n      renameAt,\n      moveAt,\n      insertInto,\n      addingAt,\n      pendingFor,\n      acceptPending,\n      rejectPending,\n    ],\n  );\n\n  return (\n    <JsonEditorContext.Provider value={context}>\n      <div\n        ref={ref}\n        \/\/ Rest FIRST: a caller's `data-*` \/ `aria-*` reach the DOM, but nothing\n        \/\/ they pass can overwrite the marker below or drop the internal classes.\n        {...rest}\n        data-react-fancy-json-editor=\"\"\n        data-issues={String(issues.length)}\n        data-keymap-error={parsedKeyMap.ok ? undefined : \"true\"}\n        data-mode={readOnly ? \"readonly\" : mode}\n        className={cn(\n          \"overflow-hidden rounded-lg border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-100\",\n          className,\n        )}\n      >\n        {showIssues && issues.length > 0 && (\n          <div data-react-fancy-json-editor-issues=\"\" className=\"p-2\">\n            <Callout color={parsedKeyMap.ok ? \"amber\" : \"red\"}>\n              <Text size=\"sm\" weight=\"semibold\" className=\"mb-1\">\n                {parsedKeyMap.ok\n                  ? `${issues.length} ${issues.length === 1 ? \"issue\" : \"issues\"}`\n                  : \"The keyMap could not be used \u2014 values are shown untyped.\"}\n              <\/Text>\n              <ul className=\"space-y-0.5\">\n                {issues.map((issue, index) => (\n                  <li key={`${issue.kind}-${issue.path}-${index}`}>\n                    <Text as=\"span\" size=\"xs\" data-react-fancy-json-editor-issue=\"\" data-kind={issue.kind} data-path={issue.path}>\n                      {issue.path ? `${issue.path} \u2014 ` : \"\"}\n                      {issue.message}\n                    <\/Text>\n                  <\/li>\n                ))}\n              <\/ul>\n            <\/Callout>\n          <\/div>\n        )}\n\n        <div data-react-fancy-json-editor-tree=\"\" role=\"tree\" aria-label={rest[\"aria-label\"] ?? \"JSON document\"}>\n          {items.length === 0 ? (\n            <Text as=\"p\" size=\"sm\" color=\"muted\" className=\"px-3 py-4\">\n              {emptyLabel}\n            <\/Text>\n          ) : (\n            items.map((item) =>\n              item.kind === \"node\" ? (\n                <JsonEditorRow key={`row:${item.node.dotted}`} node={item.node} \/>\n              ) : (\n                <JsonEditorAddForm\n                  key={`add:${item.path}`}\n                  path={item.path}\n                  kind={item.containerKind}\n                  depth={item.depth}\n                  suggestedType={item.suggestedType}\n                \/>\n              ),\n            )\n          )}\n        <\/div>\n\n        {orphanPending.length > 0 && (\n          <div data-react-fancy-json-editor-orphan-pending=\"\" className=\"border-t border-zinc-100 p-2 dark:border-zinc-800\">\n            {orphanPending.map((edit) => (\n              <div key={edit.id} className=\"flex items-center gap-2 py-0.5\">\n                <Text as=\"span\" size=\"xs\" color=\"muted\" className=\"min-w-0 flex-1 truncate font-mono\">\n                  {edit.label ?? describeEdit(edit)}\n                <\/Text>\n                <Button\n                  size=\"xs\"\n                  color=\"emerald\"\n                  data-react-fancy-json-editor-accept=\"\"\n                  data-id={edit.id}\n                  onClick={() => acceptPending(edit.id)}\n                >\n                  Accept\n                <\/Button>\n                <Button\n                  size=\"xs\"\n                  variant=\"ghost\"\n                  data-react-fancy-json-editor-reject=\"\"\n                  data-id={edit.id}\n                  onClick={() => rejectPending(edit.id)}\n                >\n                  Reject\n                <\/Button>\n              <\/div>\n            ))}\n          <\/div>\n        )}\n\n        {rootIsContainer && allowAdd && !readOnly && (\n          <div className=\"border-t border-zinc-100 p-2 dark:border-zinc-800\">\n            <Button\n              variant=\"ghost\"\n              size=\"xs\"\n              icon=\"plus\"\n              data-react-fancy-json-editor-add=\"\"\n              data-path=\"\"\n              onClick={() => setAddingAt(addingAt === \"\" ? null : \"\")}\n            >\n              {rootKind === \"array\" ? \"Append item\" : \"Add key\"}\n            <\/Button>\n          <\/div>\n        )}\n      <\/div>\n    <\/JsonEditorContext.Provider>\n  );\n});\n\ninterface BuildItemsOptions {\n  value: JsonValue;\n  rules: JsonKeyMapEntry[];\n  conflicts: JsonEditorIssue[];\n  isExpanded: (dotted: string) => boolean;\n  addingAt: string | null;\n  rootLabel: string;\n}\n\nfunction buildItems({\n  value,\n  rules,\n  conflicts,\n  isExpanded,\n  addingAt,\n  rootLabel,\n}: BuildItemsOptions): RenderItem[] {\n  const out: RenderItem[] = [];\n  const conflictByPath = new Map<string, JsonEditorIssue>();\n  for (const issue of conflicts) {\n    if (issue.kind === \"type\" && !conflictByPath.has(issue.path)) conflictByPath.set(issue.path, issue);\n  }\n\n  const makeNode = (\n    segments: JsonPath,\n    node: JsonValue,\n    depth: number,\n    parentKind: JsonEditorNode[\"parentKind\"],\n  ): JsonEditorNode => {\n    const rule = segments.length > 0 ? resolveKeyRule(rules, segments) : undefined;\n    const type = rule?.type ?? inferType(node);\n    const dotted = pathToString(segments);\n    const conflict = conflictByPath.get(dotted);\n    \/\/ A `json` node is raw text by definition, and a node in conflict must show\n    \/\/ what it really holds \u2014 neither is a branch to walk into.\n    const container = !conflict && type !== \"json\" && isContainerValue(node);\n\n    return {\n      path: segments,\n      dotted,\n      key: segments[segments.length - 1] ?? rootLabel,\n      depth,\n      value: node,\n      parentKind,\n      rule,\n      type,\n      declared: rule !== undefined,\n      container,\n      childCount: isJsonArray(node)\n        ? node.length\n        : isJsonObject(node)\n          ? Object.keys(node).length\n          : 0,\n      conflict,\n    };\n  };\n\n  const emitChildren = (node: JsonValue, segments: JsonPath, depth: number) => {\n    if (isJsonArray(node)) {\n      node.forEach((child, index) => emit(child, [...segments, String(index)], depth, \"array\"));\n    } else if (isJsonObject(node)) {\n      for (const [key, child] of Object.entries(node)) {\n        emit(child, [...segments, key], depth, \"object\");\n      }\n    }\n  };\n\n  const emit = (\n    node: JsonValue,\n    segments: JsonPath,\n    depth: number,\n    parentKind: JsonEditorNode[\"parentKind\"],\n  ) => {\n    const built = makeNode(segments, node, depth, parentKind);\n    out.push({ kind: \"node\", node: built });\n\n    if (!built.container || !isExpanded(built.dotted)) return;\n\n    emitChildren(node, segments, depth + 1);\n    if (addingAt === built.dotted) {\n      out.push({\n        kind: \"add\",\n        path: built.dotted,\n        containerKind: isJsonArray(node) ? \"array\" : \"object\",\n        depth: depth + 1,\n        suggestedType: resolveKeyRule(rules, [...segments, \"*\"])?.type,\n      });\n    }\n  };\n\n  if (isContainerValue(value)) {\n    emitChildren(value, [], 0);\n    if (addingAt === \"\") {\n      out.push({\n        kind: \"add\",\n        path: \"\",\n        containerKind: isJsonArray(value) ? \"array\" : \"object\",\n        depth: 0,\n        suggestedType: resolveKeyRule(rules, [\"*\"])?.type,\n      });\n    }\n  } else {\n    out.push({ kind: \"node\", node: makeNode([], value, 0, \"root\") });\n  }\n\n  return out;\n}\n","type":"registry:ui","target":"components\/fancy\/json-editor\/JsonEditor.tsx"},{"path":"components\/fancy\/json-editor\/JsonEditor.types.ts","content":"import type { HTMLAttributes } from \"react\";\nimport type { InputOption } from \"..\/inputs\/inputs.types\";\nimport type { FieldMode } from \"..\/inputs\/inputs.types\";\nimport type { Size } from \"..\/..\/utils\/types\";\n\n\/\/ ---------------------------------------------------------------------------\n\/\/ JSON\n\/\/ ---------------------------------------------------------------------------\n\nexport type JsonPrimitive = string | number | boolean | null;\n\nexport interface JsonObject {\n  [key: string]: JsonValue;\n}\n\nexport type JsonArray = JsonValue[];\n\nexport type JsonValue = JsonPrimitive | JsonObject | JsonArray;\n\n\/**\n * A location inside a JSON document, one array element per segment. Array\n * indices are decimal STRINGS (`[\"orders\", \"0\", \"total\"]`) so a path is a plain\n * `string[]` an agent can emit without a tagged union, and so object keys and\n * array indices are addressed identically.\n *\n * The wire form is the dotted string produced by `pathToString` \u2014 see\n * `JsonEditor.paths.ts` for the escaping rule.\n *\/\nexport type JsonPath = string[];\n\n\/\/ ---------------------------------------------------------------------------\n\/\/ Declared types\n\/\/ ---------------------------------------------------------------------------\n\n\/**\n * The data types a `keyMap` can impose on a JSON value.\n *\n * Each one owes BOTH a read-only representation and an edit control \u2014 that\n * pairing is the whole point of the feature, so a type with no distinct input\n * does not earn a place here.\n *\n * `json` is the escape hatch: it matches any value and edits as raw text, which\n * is how a sub-document opts out of the row-per-key treatment.\n *\/\nexport const JSON_FIELD_TYPES = [\n  \"string\",\n  \"text\",\n  \"number\",\n  \"integer\",\n  \"boolean\",\n  \"date\",\n  \"datetime\",\n  \"enum\",\n  \"secret\",\n  \"url\",\n  \"email\",\n  \"color\",\n  \"json\",\n  \"object\",\n  \"array\",\n] as const;\n\nexport type JsonFieldType = (typeof JSON_FIELD_TYPES)[number];\n\n\/**\n * The long form of a `keyMap` entry. The short form \u2014 a bare type name \u2014 is\n * sugar for `{ type }`.\n *\/\nexport interface JsonKeyRule {\n  type: JsonFieldType;\n  \/** Overrides the raw key as the row's label. *\/\n  label?: string;\n  \/** Choices for `type: \"enum\"`. Required there; ignored elsewhere. *\/\n  options?: InputOption[];\n  \/** Render the value but never offer a control for it. *\/\n  readOnly?: boolean;\n  \/** The key must be present. Only enforced on wildcard-free patterns. *\/\n  required?: boolean;\n  description?: string;\n  placeholder?: string;\n  \/** Forwarded to the control for `number` \/ `integer` \/ `date` \/ `datetime`. *\/\n  min?: number | string;\n  max?: number | string;\n}\n\n\/** One compiled `keyMap` entry: a path pattern plus the rule it imposes. *\/\nexport interface JsonKeyMapEntry {\n  \/** Path pattern; a `\"*\"` segment matches any single key or index. *\/\n  pattern: JsonPath;\n  rule: JsonKeyRule;\n  \/** The key exactly as written in the `keyMap`, for error messages. *\/\n  source: string;\n  \/** Non-wildcard segment count \u2014 the specificity score. *\/\n  literals: number;\n  \/** Declaration order, the tie-breaker at equal specificity. *\/\n  order: number;\n}\n\nexport interface ParsedKeyMap {\n  \/**\n   * `false` only when the STRING itself was unusable (unparseable, or valid\n   * JSON of the wrong shape). Individual bad rules leave this `true` \u2014 they are\n   * reported and skipped so one typo cannot silently disable a whole map.\n   *\/\n  ok: boolean;\n  rules: JsonKeyMapEntry[];\n  issues: JsonEditorIssue[];\n}\n\n\/\/ ---------------------------------------------------------------------------\n\/\/ Issues\n\/\/ ---------------------------------------------------------------------------\n\n\/**\n * Everything the editor knows to be wrong, in one shape.\n *\n * A typing feature that silently stops typing things is worse than no typing at\n * all, because the caller believes the constraint is in force. So a broken\n * `keyMap`, a broken rule inside a working one, and a value that contradicts\n * its declared type all surface through the SAME channel \u2014 the panel, the\n * `data-issues` count, and `onIssuesChange`.\n *\/\nexport interface JsonEditorIssue {\n  \/**\n   * - `keymap` \u2014 the `keyMap` string could not be used at all.\n   * - `rule`   \u2014 one entry in an otherwise-usable map is malformed.\n   * - `type`   \u2014 a value contradicts the type declared for its path.\n   * - `edit`   \u2014 a structural edit was refused (duplicate key, and the like).\n   *\/\n  kind: \"keymap\" | \"rule\" | \"type\" | \"edit\";\n  \/** Dotted path; `\"\"` for the `keyMap` as a whole. *\/\n  path: string;\n  message: string;\n  \/** The declared type, when the issue has one. *\/\n  expected?: JsonFieldType;\n  \/** What the value actually is: `\"string\"`, `\"array\"`, `\"null\"`, \u2026 *\/\n  actual?: string;\n}\n\n\/\/ ---------------------------------------------------------------------------\n\/\/ Edits\n\/\/ ---------------------------------------------------------------------------\n\n\/**\n * One mutation, as data.\n *\n * Every change the UI makes is expressed as one of these and applied through\n * `applyJsonEdit`, which means an MCP bridge can replay exactly what a human\n * did \u2014 and `pendingMode` can hold one in a queue instead of applying it \u2014\n * without a second code path.\n *\/\nexport type JsonEditorEditBase = {\n  id?: string;\n  \/** Human-readable summary, used by the pending strip. *\/\n  label?: string;\n};\n\nexport type JsonEditorEdit =\n  | (JsonEditorEditBase & { op: \"set\"; path: string; value: JsonValue })\n  | (JsonEditorEditBase & { op: \"remove\"; path: string })\n  | (JsonEditorEditBase & { op: \"rename\"; path: string; key: string })\n  \/** `path` is the CONTAINER. `key` is required for an object, ignored for an array. *\/\n  | (JsonEditorEditBase & { op: \"insert\"; path: string; key?: string; value: JsonValue })\n  \/** `path` is the element; `to` is its new index in the same array. *\/\n  | (JsonEditorEditBase & { op: \"move\"; path: string; to: number });\n\nexport type JsonEditorPendingEdit = JsonEditorEdit & { id: string };\n\nexport interface JsonEditorActivity {\n  \/** `commit` \u2014 applied to the document. `stage` \/ `accept` \/ `reject` \u2014 pending flow. *\/\n  type: \"commit\" | \"stage\" | \"accept\" | \"reject\";\n  edit: JsonEditorEdit;\n}\n\n\/\/ ---------------------------------------------------------------------------\n\/\/ Rows\n\/\/ ---------------------------------------------------------------------------\n\n\/** One rendered key\/value row \u2014 also the unit a bridge would address. *\/\nexport interface JsonEditorNode {\n  path: JsonPath;\n  \/** `pathToString(path)`; `\"\"` for a scalar root. *\/\n  dotted: string;\n  \/** The last segment: an object key or an array index. *\/\n  key: string;\n  depth: number;\n  value: JsonValue;\n  parentKind: \"object\" | \"array\" | \"root\";\n  \/** The rule that matched, if any. *\/\n  rule?: JsonKeyRule;\n  \/** Declared type if a rule matched, otherwise inferred from the value. *\/\n  type: JsonFieldType;\n  declared: boolean;\n  \/** Renders as a collapsible branch rather than a value. *\/\n  container: boolean;\n  childCount: number;\n  \/** Set when the value contradicts its declared type. *\/\n  conflict?: JsonEditorIssue;\n}\n\n\/\/ ---------------------------------------------------------------------------\n\/\/ Props\n\/\/ ---------------------------------------------------------------------------\n\nexport interface JsonEditorProps\n  extends Omit<HTMLAttributes<HTMLDivElement>, \"onChange\" | \"defaultValue\"> {\n  \/** The document. Controlled \u2014 the editor keeps no copy of it. *\/\n  value: JsonValue;\n  \/**\n   * Called with the WHOLE next document plus the edit that produced it. The\n   * edit is what a bridge would log, replay, or undo.\n   *\/\n  onChange?: (value: JsonValue, edit: JsonEditorEdit) => void;\n  \/**\n   * Type declarations, as a **JSON string** \u2014 not an object, and an object is\n   * NOT accepted as a convenience overload.\n   *\n   * A string survives every boundary this prop actually crosses: an MCP tool\n   * argument, a config column, a `data-*` attribute, a form field. A live\n   * object survives none of them, so accepting both would make the documented\n   * form the second-class one in practice.\n   *\n   * ```jsonc\n   * {\n   *   \"user.age\":       \"number\",           \/\/ short form: a bare type name\n   *   \"tags.*\":         \"string\",           \/\/ every element of an array\n   *   \"orders.*.total\": \"number\",\n   *   \"role\": { \"type\": \"enum\", \"options\": [\"admin\", \"member\"] }\n   * }\n   * ```\n   *\n   * Malformed input is never thrown and never silently ignored \u2014 see\n   * {@link JsonEditorIssue}.\n   *\/\n  keyMap?: string | null;\n  \/**\n   * `\"view\"` (the default here) renders values as text that turns into the\n   * typed control when clicked; `\"edit\"` renders every control at once.\n   *\n   * The kit default is `\"edit\"`, and this component deliberately differs: a\n   * forty-key document drawn as forty boxed inputs is not a document you can\n   * read, and reading is most of what a JSON editor is for.\n   *\/\n  mode?: FieldMode;\n  size?: Size;\n  \/** Show every value, offer no control. Overrides `mode` and every `allow*`. *\/\n  readOnly?: boolean;\n  \/**\n   * Dotted paths of the expanded containers. Omit BOTH this and\n   * `defaultExpanded` to expand everything (pass `defaultExpanded={[]}` for a\n   * large document).\n   *\/\n  expanded?: string[];\n  defaultExpanded?: string[];\n  onExpandedChange?: (expanded: string[]) => void;\n  \/** Render the issues panel. The `data-issues` count and `onIssuesChange` are unaffected. *\/\n  showIssues?: boolean;\n  onIssuesChange?: (issues: JsonEditorIssue[]) => void;\n  \/**\n   * Trust-but-verify. With this on, an edit is STAGED into `pending` instead of\n   * being applied: `onChange` does not fire until a human accepts it.\n   *\/\n  pendingMode?: boolean;\n  \/** Staged edits. Controlled, so an agent's proposals are inspectable. *\/\n  pending?: JsonEditorPendingEdit[];\n  onPendingChange?: (pending: JsonEditorPendingEdit[]) => void;\n  \/** Every mutation, staged or applied \u2014 the hook presence \/ undo layers listen on. *\/\n  onActivity?: (event: JsonEditorActivity) => void;\n  allowAdd?: boolean;\n  allowRemove?: boolean;\n  allowRename?: boolean;\n  allowReorder?: boolean;\n  \/** Label for a scalar root document. Default `\"value\"`. *\/\n  rootLabel?: string;\n  \/** Shown when the document has no keys. *\/\n  emptyLabel?: string;\n  \/** Prefix for generated control ids \u2014 `<idPrefix>-<dotted path>`. *\/\n  idPrefix?: string;\n}\n","type":"registry:ui","target":"components\/fancy\/json-editor\/JsonEditor.types.ts"},{"path":"components\/fancy\/json-editor\/JsonEditorRow.tsx","content":"import { useState } from \"react\";\nimport { cn } from \"..\/..\/utils\/cn\";\nimport { Badge } from \"..\/Badge\/Badge\";\nimport { Button } from \"..\/Button\/Button\";\nimport { Text } from \"..\/Text\/Text\";\nimport { Input } from \"..\/inputs\/Input\/Input\";\nimport { Select } from \"..\/inputs\/Select\/Select\";\nimport { useJsonEditorContext } from \".\/JsonEditor.context\";\nimport { describeEdit } from \".\/JsonEditor.paths\";\nimport { JsonEditorValue, toText } from \".\/JsonEditorValue\";\nimport { JSON_FIELD_TYPES } from \".\/JsonEditor.types\";\nimport type { JsonEditorNode, JsonFieldType } from \".\/JsonEditor.types\";\n\n\/** One indent step, in px. Applied as an inline style because depth is data. *\/\nconst INDENT = 18;\n\nexport interface JsonEditorRowProps {\n  node: JsonEditorNode;\n}\n\n\/**\n * One key\/value row.\n *\n * Flat in the DOM rather than nested: `aria-level` carries the depth, so an\n * agent can address `[data-path=\"orders.2.total\"]` in one selector instead of\n * walking a nesting it would have to guess the shape of. It is also the\n * standard flat-tree ARIA pattern.\n *\/\nexport function JsonEditorRow({ node }: JsonEditorRowProps) {\n  const ctx = useJsonEditorContext();\n  const { dotted, key, depth, container, conflict } = node;\n\n  const expanded = container ? ctx.isExpanded(dotted) : undefined;\n  const pending = ctx.pendingFor(dotted);\n  const renameable =\n    ctx.allowRename && !ctx.readOnly && node.parentKind === \"object\" && ctx.mode === \"edit\";\n  const label = node.rule?.label ?? key;\n\n  return (\n    <div\n      data-react-fancy-json-editor-row=\"\"\n      data-path={dotted}\n      data-key={key}\n      data-type={node.type}\n      data-depth={depth}\n      data-declared={node.declared ? \"true\" : \"false\"}\n      data-conflict={conflict ? \"true\" : undefined}\n      data-pending={pending.length > 0 ? \"true\" : undefined}\n      role=\"treeitem\"\n      aria-level={depth + 1}\n      aria-expanded={expanded}\n      className={cn(\n        \"border-b border-zinc-100 py-1 last:border-b-0 dark:border-zinc-800\",\n        conflict && \"bg-red-50\/60 dark:bg-red-950\/20\",\n        pending.length > 0 && \"bg-violet-50\/60 dark:bg-violet-950\/20\",\n      )}\n    >\n      <div className=\"flex items-start gap-2 px-2\" style={{ paddingInlineStart: depth * INDENT + 8 }}>\n        {container ? (\n          <Button\n            variant=\"ghost\"\n            size=\"xs\"\n            data-react-fancy-json-editor-toggle=\"\"\n            data-path={dotted}\n            aria-label={expanded ? `Collapse ${label}` : `Expand ${label}`}\n            aria-expanded={expanded}\n            icon={expanded ? \"chevron-down\" : \"chevron-right\"}\n            onClick={() => ctx.toggleExpanded(dotted)}\n          \/>\n        ) : (\n          <span aria-hidden=\"true\" className=\"inline-block w-6 shrink-0\" \/>\n        )}\n\n        <div\n          data-react-fancy-json-editor-key=\"\"\n          data-path={dotted}\n          className=\"w-1\/3 min-w-0 shrink-0\"\n        >\n          {renameable ? (\n            <Input\n              \/\/ Uncontrolled + keyed + commit on blur: renaming per keystroke\n              \/\/ would fire one structural edit per character.\n              key={key}\n              size={ctx.size}\n              mode=\"edit\"\n              defaultValue={key}\n              spellCheck={false}\n              aria-label={`Key of ${dotted}`}\n              onKeyDown={(event) => {\n                if (event.key === \"Enter\") event.currentTarget.blur();\n              }}\n              onBlur={(event) => {\n                const next = event.currentTarget.value;\n                if (next !== key) ctx.renameAt(node, next);\n              }}\n            \/>\n          ) : (\n            <Text\n              as=\"span\"\n              size=\"sm\"\n              weight=\"medium\"\n              color={node.parentKind === \"array\" ? \"muted\" : \"default\"}\n              className=\"block truncate py-1 font-mono\"\n              title={dotted}\n            >\n              {node.parentKind === \"array\" ? `[${key}]` : label}\n            <\/Text>\n          )}\n          {node.rule?.description && (\n            <Text as=\"span\" size=\"xs\" color=\"muted\" className=\"block truncate\">\n              {node.rule.description}\n            <\/Text>\n          )}\n        <\/div>\n\n        <div\n          data-react-fancy-json-editor-value=\"\"\n          data-path={dotted}\n          data-type={node.type}\n          className=\"min-w-0 flex-1\"\n        >\n          {container ? (\n            <Text as=\"span\" size=\"sm\" color=\"muted\" className=\"block py-1 font-mono\">\n              {node.type === \"array\"\n                ? `[ ${node.childCount} ${node.childCount === 1 ? \"item\" : \"items\"} ]`\n                : `{ ${node.childCount} ${node.childCount === 1 ? \"key\" : \"keys\"} }`}\n            <\/Text>\n          ) : (\n            <JsonEditorValue\n              node={node}\n              mode={ctx.mode}\n              size={ctx.size}\n              readOnly={ctx.readOnly}\n              id={`${ctx.idPrefix}-${dotted}`}\n              onCommit={(next) => ctx.setValueAt(node, next)}\n            \/>\n          )}\n        <\/div>\n\n        <div className=\"flex shrink-0 items-center gap-0.5 pt-0.5\">\n          {node.declared && (\n            <Badge size=\"sm\" variant=\"soft\" color={conflict ? \"red\" : \"zinc\"}>\n              {node.type}\n            <\/Badge>\n          )}\n          {container && ctx.allowAdd && !ctx.readOnly && (\n            <Button\n              variant=\"ghost\"\n              size=\"xs\"\n              data-react-fancy-json-editor-add=\"\"\n              data-path={dotted}\n              aria-label={`Add to ${label}`}\n              icon=\"plus\"\n              onClick={() => ctx.setAddingAt(ctx.addingAt === dotted ? null : dotted)}\n            \/>\n          )}\n          {ctx.allowReorder && !ctx.readOnly && node.parentKind === \"array\" && (\n            <>\n              <Button\n                variant=\"ghost\"\n                size=\"xs\"\n                data-react-fancy-json-editor-move-up=\"\"\n                data-path={dotted}\n                aria-label={`Move ${dotted} up`}\n                icon=\"arrow-up\"\n                onClick={() => ctx.moveAt(node, Number(key) - 1)}\n              \/>\n              <Button\n                variant=\"ghost\"\n                size=\"xs\"\n                data-react-fancy-json-editor-move-down=\"\"\n                data-path={dotted}\n                aria-label={`Move ${dotted} down`}\n                icon=\"arrow-down\"\n                onClick={() => ctx.moveAt(node, Number(key) + 1)}\n              \/>\n            <\/>\n          )}\n          {ctx.allowRemove && !ctx.readOnly && node.parentKind !== \"root\" && (\n            <Button\n              variant=\"ghost\"\n              size=\"xs\"\n              color=\"red\"\n              data-react-fancy-json-editor-remove=\"\"\n              data-path={dotted}\n              aria-label={`Remove ${dotted}`}\n              icon=\"trash-2\"\n              onClick={() => ctx.removeAt(node)}\n            \/>\n          )}\n        <\/div>\n      <\/div>\n\n      {conflict && (\n        <Text\n          as=\"span\"\n          size=\"xs\"\n          color=\"danger\"\n          data-react-fancy-json-editor-conflict=\"\"\n          data-path={dotted}\n          className=\"block px-2 pb-1\"\n          style={{ paddingInlineStart: depth * INDENT + 40 }}\n        >\n          {conflict.message}\n        <\/Text>\n      )}\n\n      {pending.map((edit) => (\n        <div\n          key={edit.id}\n          data-react-fancy-json-editor-pending=\"\"\n          data-id={edit.id}\n          data-path={dotted}\n          className=\"flex items-center gap-2 px-2 pb-1\"\n          style={{ paddingInlineStart: depth * INDENT + 40 }}\n        >\n          <Badge size=\"sm\" variant=\"soft\" color=\"violet\">\n            proposed\n          <\/Badge>\n          <Text as=\"span\" size=\"xs\" color=\"muted\" className=\"min-w-0 truncate font-mono\">\n            {edit.label ?? describeEdit(edit)}\n            {edit.op === \"set\" || edit.op === \"insert\" ? ` \u2192 ${toText(edit.value)}` : \"\"}\n          <\/Text>\n          <Button\n            size=\"xs\"\n            color=\"emerald\"\n            data-react-fancy-json-editor-accept=\"\"\n            data-id={edit.id}\n            onClick={() => ctx.acceptPending(edit.id)}\n          >\n            Accept\n          <\/Button>\n          <Button\n            size=\"xs\"\n            variant=\"ghost\"\n            data-react-fancy-json-editor-reject=\"\"\n            data-id={edit.id}\n            onClick={() => ctx.rejectPending(edit.id)}\n          >\n            Reject\n          <\/Button>\n        <\/div>\n      ))}\n    <\/div>\n  );\n}\n\nexport interface JsonEditorAddFormProps {\n  \/** Dotted path of the container being added to; `\"\"` is the document root. *\/\n  path: string;\n  kind: \"object\" | \"array\";\n  depth: number;\n  \/** Declared type for the container's children, when the keyMap names one. *\/\n  suggestedType?: JsonFieldType;\n}\n\n\/**\n * The inline \"add a key\" form.\n *\n * Its open\/closed state and its two draft fields are the only state this\n * component owns that is not in `value` \u2014 and deliberately so: a half-typed key\n * name is not part of the document, and an agent adding a key does it through\n * an `insert` edit, never by driving this form.\n *\/\nexport function JsonEditorAddForm({ path, kind, depth, suggestedType }: JsonEditorAddFormProps) {\n  const ctx = useJsonEditorContext();\n  const [key, setKey] = useState(\"\");\n  const [type, setType] = useState<JsonFieldType>(suggestedType ?? \"string\");\n\n  return (\n    <div\n      data-react-fancy-json-editor-add-form=\"\"\n      data-path={path}\n      className=\"flex items-center gap-2 border-b border-zinc-100 px-2 py-1.5 dark:border-zinc-800\"\n      style={{ paddingInlineStart: depth * INDENT + 32 }}\n    >\n      {kind === \"object\" && (\n        <Input\n          size={ctx.size}\n          mode=\"edit\"\n          value={key}\n          placeholder=\"key\"\n          aria-label=\"New key\"\n          spellCheck={false}\n          data-react-fancy-json-editor-add-key=\"\"\n          onValueChange={setKey}\n        \/>\n      )}\n      <Select\n        size={ctx.size}\n        mode=\"edit\"\n        aria-label=\"New value type\"\n        data-react-fancy-json-editor-add-type=\"\"\n        list={[...JSON_FIELD_TYPES]}\n        value={type}\n        onValueChange={(next) => setType(next as JsonFieldType)}\n      \/>\n      <Button\n        size=\"xs\"\n        color=\"blue\"\n        data-react-fancy-json-editor-add-confirm=\"\"\n        onClick={() => ctx.insertInto(path, kind === \"object\" ? key : undefined, type)}\n      >\n        Add\n      <\/Button>\n      <Button\n        size=\"xs\"\n        variant=\"ghost\"\n        data-react-fancy-json-editor-add-cancel=\"\"\n        onClick={() => ctx.setAddingAt(null)}\n      >\n        Cancel\n      <\/Button>\n    <\/div>\n  );\n}\n","type":"registry:ui","target":"components\/fancy\/json-editor\/JsonEditorRow.tsx"},{"path":"components\/fancy\/json-editor\/JsonEditorValue.tsx","content":"import { ColorPicker } from \"..\/ColorPicker\/ColorPicker\";\nimport { Input } from \"..\/inputs\/Input\/Input\";\nimport { Textarea } from \"..\/inputs\/Textarea\/Textarea\";\nimport { Select } from \"..\/inputs\/Select\/Select\";\nimport { Switch } from \"..\/inputs\/Switch\/Switch\";\nimport { DatePicker } from \"..\/inputs\/DatePicker\/DatePicker\";\nimport { DisplayValue } from \"..\/inputs\/mode\/DisplayValue\";\nimport { useInlineEdit } from \"..\/inputs\/mode\/useInlineEdit\";\nimport { resolveOption } from \"..\/inputs\/inputs.utils\";\nimport type { FieldMode } from \"..\/inputs\/inputs.types\";\nimport type { Size } from \"..\/..\/utils\/types\";\nimport type { JsonEditorNode, JsonFieldType, JsonValue } from \".\/JsonEditor.types\";\n\nexport interface JsonEditorValueProps {\n  node: JsonEditorNode;\n  mode: FieldMode;\n  size: Size;\n  \/** No control at all \u2014 the row still shows the value. *\/\n  readOnly: boolean;\n  \/** Stable control id: `<idPrefix>-<dotted path>`. *\/\n  id: string;\n  onCommit: (value: JsonValue) => void;\n}\n\nconst COLOR_PICKER_SIZE: Record<Size, \"sm\" | \"md\" | \"lg\"> = {\n  xs: \"sm\",\n  sm: \"sm\",\n  md: \"md\",\n  lg: \"lg\",\n  xl: \"lg\",\n};\n\n\/**\n * Types whose text form IS the value commit on every keystroke; types that must\n * be PARSED out of text commit on blur instead.\n *\n * That split is not fussiness. A controlled `number` field that parses per\n * keystroke turns `1.5` into `1` the moment you type the dot, and moves the\n * caret while you are still typing. Committing on blur means the intermediate\n * text is never anybody's value.\n *\/\nfunction isRawTextType(type: JsonFieldType): boolean {\n  return type === \"number\" || type === \"integer\" || type === \"json\";\n}\n\n\/** The text shown for a value in a raw editor \u2014 strings verbatim, everything else as JSON. *\/\nexport function toText(value: JsonValue | undefined): string {\n  if (value === undefined) return \"\";\n  if (typeof value === \"string\") return value;\n  return JSON.stringify(value) ?? \"\";\n}\n\n\/**\n * Read a raw editor's text back into a value.\n *\n * Text that will not parse as the declared type is stored AS TEXT. That is the\n * deliberate choice at the heart of this component: typing `abc` into a field\n * declared `number` neither coerces to `0`\/`NaN` nor throws the keystrokes\n * away \u2014 it writes `\"abc\"` and lets the conflict machinery say so, in the same\n * place it reports bad data that arrived from the server.\n *\/\nexport function fromText(text: string, type: JsonFieldType): JsonValue {\n  switch (type) {\n    case \"number\":\n    case \"integer\": {\n      const trimmed = text.trim();\n      if (trimmed === \"\") return text;\n      const parsed = Number(trimmed);\n      return Number.isFinite(parsed) ? parsed : text;\n    }\n    case \"boolean\": {\n      const trimmed = text.trim().toLowerCase();\n      if (trimmed === \"true\") return true;\n      if (trimmed === \"false\") return false;\n      return text;\n    }\n    case \"json\":\n    case \"object\":\n    case \"array\":\n      try {\n        return JSON.parse(text) as JsonValue;\n      } catch {\n        return text;\n      }\n    default:\n      return text;\n  }\n}\n\n\/**\n * The read-or-edit control for one leaf value.\n *\n * Everything here is a `react-fancy` primitive \u2014 `Input`, `Textarea`, `Select`,\n * `Switch`, `DatePicker`, `ColorPicker` \u2014 and the view\/edit swap is the kit's\n * own `useInlineEdit` + `DisplayValue`, not a private re-implementation of it.\n *\/\nexport function JsonEditorValue({ node, mode, size, readOnly, id, onCommit }: JsonEditorValueProps) {\n  const locked = readOnly || node.rule?.readOnly === true;\n  const resolvedMode: FieldMode = locked ? \"view\" : mode;\n  const { value, rule } = node;\n\n  \/\/ A value that contradicts its declared type cannot be shown through that\n  \/\/ type's control \u2014 a number input cannot hold \"thirty-six\" \u2014 so it falls back\n  \/\/ to the raw editor, which is also the only place it can be repaired.\n  if (node.conflict || isRawTextType(node.type)) {\n    return (\n      <RawValueEditor\n        id={id}\n        text={toText(value)}\n        type={node.type}\n        mode={resolvedMode}\n        size={size}\n        disabled={locked}\n        multiline={node.type === \"json\" || typeof value === \"object\"}\n        placeholder={rule?.placeholder}\n        onCommitText={(text) => onCommit(fromText(text, node.type))}\n      \/>\n    );\n  }\n\n  const common = { id, size, mode: resolvedMode, disabled: locked } as const;\n\n  switch (node.type) {\n    case \"boolean\":\n      return (\n        <Switch\n          {...common}\n          checked={value === true}\n          onCheckedChange={(next) => onCommit(next)}\n        \/>\n      );\n\n    case \"enum\": {\n      const options = rule?.options ?? [];\n      return (\n        <Select\n          {...common}\n          list={options}\n          value={String(value)}\n          onValueChange={(next) => {\n            \/\/ Emit the option's ORIGINAL value, so a numeric or boolean enum\n            \/\/ does not silently become a string on its way through the DOM.\n            const match = options\n              .map((option) => resolveOption(option))\n              .find((option) => String(option.value) === next);\n            onCommit((match?.value ?? next) as JsonValue);\n          }}\n        \/>\n      );\n    }\n\n    case \"date\":\n    case \"datetime\":\n      return (\n        <DatePicker\n          {...common}\n          includeTime={node.type === \"datetime\"}\n          min={rule?.min === undefined ? undefined : String(rule.min)}\n          max={rule?.max === undefined ? undefined : String(rule.max)}\n          value={typeof value === \"string\" ? value : \"\"}\n          onValueChange={(next) => onCommit(next)}\n        \/>\n      );\n\n    case \"color\":\n      return (\n        <ColorPicker\n          size={COLOR_PICKER_SIZE[size]}\n          mode={resolvedMode}\n          disabled={locked}\n          value={typeof value === \"string\" ? value : \"\"}\n          onChange={(next) => onCommit(next)}\n        \/>\n      );\n\n    case \"text\":\n      return (\n        <Textarea\n          {...common}\n          minRows={2}\n          autoResize\n          placeholder={rule?.placeholder}\n          value={typeof value === \"string\" ? value : \"\"}\n          onValueChange={(next) => onCommit(next)}\n        \/>\n      );\n\n    case \"secret\":\n      return (\n        <Input\n          {...common}\n          type=\"password\"\n          reveal\n          placeholder={rule?.placeholder}\n          value={typeof value === \"string\" ? value : \"\"}\n          onValueChange={(next) => onCommit(next)}\n        \/>\n      );\n\n    case \"url\":\n    case \"email\":\n    case \"string\":\n    default:\n      return (\n        <Input\n          {...common}\n          type={node.type === \"url\" ? \"url\" : node.type === \"email\" ? \"email\" : \"text\"}\n          placeholder={rule?.placeholder}\n          value={typeof value === \"string\" ? value : \"\"}\n          onValueChange={(next) => onCommit(next)}\n        \/>\n      );\n  }\n}\n\ninterface RawValueEditorProps {\n  id: string;\n  text: string;\n  type: JsonFieldType;\n  mode: FieldMode;\n  size: Size;\n  disabled: boolean;\n  multiline: boolean;\n  placeholder?: string;\n  onCommitText: (text: string) => void;\n}\n\n\/**\n * An uncontrolled text control that commits on blur.\n *\n * Uncontrolled is what keeps the component free of a private copy of the\n * document: the half-typed text lives in the DOM node, never in React state,\n * and the `key` is the committed text \u2014 so a value changed from OUTSIDE (an\n * agent writing through a bridge, a server push) remounts the control with the\n * new text, while local typing does not.\n *\/\nfunction RawValueEditor({\n  id,\n  text,\n  type,\n  mode,\n  size,\n  disabled,\n  multiline,\n  placeholder,\n  onCommitText,\n}: RawValueEditorProps) {\n  const { showControl, interactive, enterEdit, exitEdit } = useInlineEdit(mode, disabled);\n\n  if (!showControl) {\n    return (\n      <DisplayValue size={size} interactive={interactive} onActivate={enterEdit}>\n        {text}\n      <\/DisplayValue>\n    );\n  }\n\n  const commit = (next: string) => {\n    if (next !== text) onCommitText(next);\n  };\n\n  if (multiline) {\n    return (\n      <Textarea\n        id={id}\n        key={text}\n        size={size}\n        mode=\"edit\"\n        minRows={3}\n        disabled={disabled}\n        placeholder={placeholder}\n        defaultValue={text}\n        spellCheck={false}\n        autoFocus={interactive}\n        className=\"font-mono\"\n        onBlur={(event) => {\n          commit(event.currentTarget.value);\n          exitEdit();\n        }}\n      \/>\n    );\n  }\n\n  return (\n    <Input\n      id={id}\n      key={text}\n      size={size}\n      mode=\"edit\"\n      type=\"text\"\n      \/\/ Not `type=\"number\"`: an `<input type=number>` reports \"\" for text it\n      \/\/ considers invalid, so the literal keystrokes we promise to keep would\n      \/\/ be unreadable on commit. The numeric keyboard is requested instead.\n      inputMode={type === \"number\" || type === \"integer\" ? \"decimal\" : undefined}\n      disabled={disabled}\n      placeholder={placeholder}\n      defaultValue={text}\n      spellCheck={false}\n      autoFocus={interactive}\n      onKeyDown={(event) => {\n        if (event.key === \"Enter\") event.currentTarget.blur();\n      }}\n      onBlur={(event) => {\n        commit(event.currentTarget.value);\n        exitEdit();\n      }}\n    \/>\n  );\n}\n","type":"registry:ui","target":"components\/fancy\/json-editor\/JsonEditorValue.tsx"},{"path":"components\/fancy\/json-editor\/index.ts","content":"export { JsonEditor } from \".\/JsonEditor\";\nexport type {\n  JsonEditorProps,\n  JsonEditorEdit,\n  JsonEditorPendingEdit,\n  JsonEditorActivity,\n  JsonEditorIssue,\n  JsonEditorNode,\n  JsonFieldType,\n  JsonKeyRule,\n  JsonKeyMapEntry,\n  ParsedKeyMap,\n  JsonValue,\n  JsonObject,\n  JsonArray,\n  JsonPrimitive,\n  JsonPath,\n} from \".\/JsonEditor.types\";\nexport {\n  parseKeyMap,\n  resolveKeyRule,\n  findJsonConflicts,\n  inferType,\n  typeMatches,\n} from \".\/JsonEditor.keymap\";\nexport {\n  applyJsonEdit,\n  describeEdit,\n  parsePath,\n  pathToString,\n  getAtPath,\n} from \".\/JsonEditor.paths\";\n","type":"registry:ui","target":"components\/fancy\/json-editor\/index.ts"}],"since":"0.5"}