{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "markdown-editor",
  "title": "Markdown Editor",
  "author": "ilinxa",
  "description": "CodeMirror 6 markdown editor with GFM, wikilink autocomplete, a slot-able toolbar, and edit, split, and preview modes.",
  "dependencies": [
    "lucide-react",
    "marked",
    "@codemirror/state",
    "@codemirror/view",
    "@codemirror/commands",
    "@codemirror/language",
    "@codemirror/lang-markdown",
    "@codemirror/autocomplete",
    "@codemirror/search",
    "@lezer/markdown",
    "@lezer/highlight"
  ],
  "registryDependencies": [
    "badge",
    "button",
    "tabs",
    "tooltip"
  ],
  "files": [
    {
      "path": "src/registry/components/forms/markdown-editor/markdown-editor.tsx",
      "content": "\"use client\";\n\nimport {\n  useDeferredValue,\n  useEffect,\n  useMemo,\n  useRef,\n} from \"react\";\nimport type { Extension } from \"@codemirror/state\";\nimport { TooltipProvider } from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\n\nimport { useCodeMirror } from \"./hooks/use-codemirror\";\nimport { useViewMode } from \"./hooks/use-view-mode\";\nimport {\n  useToolbarStore,\n  useToolbarVersion,\n} from \"./hooks/use-toolbar-state\";\nimport { useMarkdownEditorHandle } from \"./hooks/use-imperative-handle\";\nimport { Toolbar } from \"./parts/toolbar\";\nimport { ViewToggle } from \"./parts/view-toggle\";\nimport { EditorPane } from \"./parts/editor-pane\";\nimport { PreviewPane } from \"./parts/preview-pane\";\nimport { defaultMarkdownToolbar } from \"./default-toolbar\";\nimport { parseMarkdown } from \"./lib/parse-markdown\";\nimport {\n  insertText as insertTextAction,\n  toggleLinePrefix as toggleLinePrefixAction,\n  wrapSelection as wrapSelectionAction,\n} from \"./lib/toolbar-actions\";\nimport type {\n  KindMeta,\n  MarkdownEditorProps,\n  ToolbarCtx,\n  WikilinkCandidate,\n} from \"./types\";\n\nconst EMPTY_EXTENSIONS: ReadonlyArray<Extension> = [];\n\nexport function MarkdownEditor<TCandidate extends WikilinkCandidate = WikilinkCandidate>(\n  props: MarkdownEditorProps<TCandidate>,\n) {\n  const {\n    value,\n    onChange,\n    readOnly = false,\n    view: viewProp,\n    onViewChange,\n    initialView = \"edit\",\n    showPreviewToggle = true,\n    wikilinkCandidates,\n    onWikilinkClick,\n    kinds,\n    toolbar,\n    extensions,\n    onSave,\n    ariaLabel,\n    className,\n    minHeight,\n    maxHeight,\n    ref,\n    id,\n  } = props;\n\n  // Stable refs holding latest callbacks (post-commit update — read by CM6 extensions\n  // captured at mount time so prop changes flow through without reconfiguring the stack).\n  const onChangeRef = useRef(onChange);\n  const onSaveRef = useRef<typeof onSave>(onSave);\n  const kindsRef = useRef<Record<string, KindMeta> | undefined>(kinds);\n  const onWikilinkClickRef = useRef<typeof onWikilinkClick>(onWikilinkClick);\n\n  useEffect(() => {\n    onChangeRef.current = onChange;\n    onSaveRef.current = onSave;\n    kindsRef.current = kinds;\n    onWikilinkClickRef.current = onWikilinkClick;\n  });\n\n  // Toolbar external store — bumped by CM6 update listener on selection or doc changes.\n  const toolbarStore = useToolbarStore();\n  const onRelevantUpdateRef = useRef<(() => void) | undefined>(toolbarStore.notify);\n  useEffect(() => {\n    onRelevantUpdateRef.current = toolbarStore.notify;\n  }, [toolbarStore]);\n\n  const userExtensions = extensions ?? EMPTY_EXTENSIONS;\n\n  const { view, setContainer } = useCodeMirror({\n    value,\n    readOnly,\n    wikilinkCandidates,\n    userExtensions,\n    onChangeRef,\n    onSaveRef,\n    kindsRef,\n    onRelevantUpdateRef,\n    ariaLabel,\n  });\n\n  const { viewValue, handleViewChange, toggleHidden } = useViewMode({\n    view: viewProp,\n    onViewChange,\n    initialView,\n    showPreviewToggle,\n  });\n\n  // Subscribe to toolbar updates — re-renders toolbar buttons on selection/doc changes.\n  useToolbarVersion(toolbarStore);\n\n  // Preview HTML — useDeferredValue keeps typing fluid; parse runs at low priority.\n  const deferredValue = useDeferredValue(value);\n  const hasClickHandler = Boolean(onWikilinkClick);\n  const html = useMemo(\n    () =>\n      parseMarkdown(deferredValue, {\n        wikilinkCandidates,\n        kinds,\n        hasClickHandler,\n      }),\n    [deferredValue, wikilinkCandidates, kinds, hasClickHandler],\n  );\n\n  // Build ToolbarCtx from current view — recomputed on render (cheap; closures over view).\n  const ctx: ToolbarCtx | null = view\n    ? {\n        view,\n        value: view.state.doc.toString(),\n        insertText: (text) => insertTextAction(view, text),\n        wrapSelection: (before, after) => wrapSelectionAction(view, before, after),\n        toggleLinePrefix: (prefix) => toggleLinePrefixAction(view, prefix),\n      }\n    : null;\n\n  const showToolbar = toolbar !== false;\n  const items = toolbar === undefined || toolbar === false ? defaultMarkdownToolbar : toolbar;\n  const showEditor = viewValue !== \"preview\";\n  const showPreview = viewValue !== \"edit\";\n\n  // Imperative handle wiring (Q-P10 ref-as-prop pattern).\n  useMarkdownEditorHandle({ ref, view });\n\n  // Onclick handler for preview wikilinks reads onWikilinkClickRef so prop changes\n  // flow through without re-binding the stable click delegation.\n  const handlePreviewWikilinkClick = useMemo(\n    () => (target: string) => {\n      onWikilinkClickRef.current?.(target);\n    },\n    [],\n  );\n\n  return (\n    <TooltipProvider>\n      <div\n        id={id}\n        role=\"group\"\n        aria-label={ariaLabel ?? \"Markdown editor\"}\n        className={cn(\"markdown-editor flex flex-col\", className)}\n      >\n        {(showToolbar || !toggleHidden) && (\n          <div\n            className={cn(\n              \"flex flex-wrap items-center justify-between gap-2\",\n              showToolbar\n                ? \"rounded-t-md border border-b-0 border-border bg-card px-2 py-1.5\"\n                : null,\n            )}\n          >\n            {showToolbar && ctx ? (\n              <Toolbar\n                items={items}\n                ctx={ctx}\n                disabled={readOnly || viewValue === \"preview\"}\n                className=\"border-0 bg-transparent p-0\"\n              />\n            ) : showToolbar ? (\n              // Pre-mount placeholder so layout doesn't shift when CM6 attaches.\n              <div className=\"h-7\" aria-hidden />\n            ) : null}\n            {!toggleHidden && (\n              <ViewToggle value={viewValue} onChange={handleViewChange} />\n            )}\n          </div>\n        )}\n\n        <div\n          className={cn(\n            \"@container flex min-h-0 overflow-hidden rounded-b-md border border-border\",\n            !showToolbar && toggleHidden && \"rounded-t-md\",\n            viewValue === \"split\"\n              ? \"flex-col @[480px]:flex-row @[480px]:gap-0\"\n              : \"flex-col\",\n          )}\n        >\n          {showEditor && (\n            <EditorPane\n              setContainer={setContainer}\n              minHeight={minHeight}\n              maxHeight={maxHeight}\n              className={cn(\n                viewValue === \"split\" && \"@[480px]:border-r @[480px]:border-border\",\n              )}\n            />\n          )}\n          {showPreview && (\n            <PreviewPane\n              html={html}\n              onWikilinkClick={hasClickHandler ? handlePreviewWikilinkClick : undefined}\n              minHeight={minHeight}\n              maxHeight={maxHeight}\n            />\n          )}\n        </div>\n      </div>\n    </TooltipProvider>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/markdown-editor/markdown-editor.tsx"
    },
    {
      "path": "src/registry/components/forms/markdown-editor/index.ts",
      "content": "export { MarkdownEditor } from \"./markdown-editor\";\nexport { defaultMarkdownToolbar } from \"./default-toolbar\";\nexport { parseMarkdown } from \"./lib/parse-markdown\";\nexport type {\n  KindMeta,\n  MarkdownEditorHandle,\n  MarkdownEditorProps,\n  ToolbarCtx,\n  ToolbarItem,\n  ViewMode,\n  WikilinkCandidate,\n} from \"./types\";\n",
      "type": "registry:component",
      "target": "components/markdown-editor/index.ts"
    },
    {
      "path": "src/registry/components/forms/markdown-editor/types.ts",
      "content": "import type { ReactNode, Ref } from \"react\";\nimport type { Extension } from \"@codemirror/state\";\nimport type { EditorView } from \"@codemirror/view\";\n\nexport interface WikilinkCandidate {\n  id: string;\n  label: string;\n  kind?: string;\n  alias?: string;\n}\n\nexport interface KindMeta {\n  label: string;\n  color?: string;\n}\n\nexport type ViewMode = \"edit\" | \"preview\" | \"split\";\n\nexport interface ToolbarCtx {\n  view: EditorView;\n  value: string;\n  insertText: (text: string) => void;\n  wrapSelection: (before: string, after?: string) => void;\n  toggleLinePrefix: (prefix: string) => void;\n}\n\nexport interface ToolbarItem {\n  id: string;\n  label: string;\n  icon?: ReactNode;\n  shortcut?: string;\n  isActive?: (ctx: ToolbarCtx) => boolean;\n  /*\n   * Optional since v0.2.1. A separator (`label: \"\"`) is a real `ToolbarItem`\n   * that the renderer draws as a divider and never invokes — but `run` was\n   * required, so the documented separator idiom told consumers to write\n   * `run: () => {}`. That taught the codebase to author empty-bodied public\n   * methods, which is the exact shape being eliminated everywhere else.\n   * Separators now simply omit it.\n   */\n  run?: (ctx: ToolbarCtx) => void;\n}\n\nexport interface MarkdownEditorProps<TCandidate extends WikilinkCandidate = WikilinkCandidate> {\n  value: string;\n  onChange: (value: string) => void;\n  readOnly?: boolean;\n\n  view?: ViewMode;\n  onViewChange?: (view: ViewMode) => void;\n  initialView?: ViewMode;\n  showPreviewToggle?: boolean;\n\n  wikilinkCandidates?: ReadonlyArray<TCandidate>;\n  onWikilinkClick?: (target: string) => void;\n  kinds?: Record<string, KindMeta>;\n\n  toolbar?: false | ReadonlyArray<ToolbarItem>;\n\n  extensions?: ReadonlyArray<Extension>;\n\n  onSave?: (value: string) => void;\n\n  ariaLabel?: string;\n  className?: string;\n  placeholder?: string;\n  minHeight?: string | number;\n  maxHeight?: string | number;\n\n  ref?: Ref<MarkdownEditorHandle>;\n\n  id?: string;\n}\n\nexport interface MarkdownEditorHandle {\n  focus(): void;\n  undo(): void;\n  redo(): void;\n  insertText(text: string): void;\n  getSelection(): { from: number; to: number; text: string };\n  getValue(): string;\n  getView(): EditorView | null;\n}\n",
      "type": "registry:component",
      "target": "components/markdown-editor/types.ts"
    },
    {
      "path": "src/registry/components/forms/markdown-editor/default-toolbar.tsx",
      "content": "import { Bold, Code, Heading, Italic, Link, List, ListOrdered, Quote } from \"lucide-react\";\nimport { cycleHeading, isInlineMarkActive } from \"./lib/toolbar-actions\";\nimport type { ToolbarItem } from \"./types\";\n\n// Default markdown toolbar — 8 built-in items + 1 separator between link and lists (Q-P4 + §13.5 #7).\n// Hosts spread + extend per description §6.2.\nexport const defaultMarkdownToolbar: ReadonlyArray<ToolbarItem> = [\n  {\n    id: \"bold\",\n    label: \"Bold\",\n    icon: <Bold />,\n    shortcut: \"⌘B\",\n    isActive: (ctx) => isInlineMarkActive(ctx.view, \"**\"),\n    run: (ctx) => ctx.wrapSelection(\"**\", \"**\"),\n  },\n  {\n    id: \"italic\",\n    label: \"Italic\",\n    icon: <Italic />,\n    shortcut: \"⌘I\",\n    run: (ctx) => ctx.wrapSelection(\"*\", \"*\"),\n  },\n  {\n    id: \"code\",\n    label: \"Inline code\",\n    icon: <Code />,\n    shortcut: \"⌘E\",\n    isActive: (ctx) => isInlineMarkActive(ctx.view, \"`\"),\n    run: (ctx) => ctx.wrapSelection(\"`\", \"`\"),\n  },\n  {\n    id: \"link\",\n    label: \"Link\",\n    icon: <Link />,\n    shortcut: \"⌘K\",\n    run: (ctx) => {\n      const { from, to } = ctx.view.state.selection.main;\n      const selectedText = ctx.view.state.sliceDoc(from, to);\n      const before = `[${selectedText}](`;\n      const placeholder = \"url\";\n      const after = \")\";\n      const insertStart = from + before.length;\n      ctx.view.dispatch({\n        changes: { from, to, insert: `${before}${placeholder}${after}` },\n        selection: { anchor: insertStart, head: insertStart + placeholder.length },\n      });\n      ctx.view.focus();\n    },\n  },\n  { id: \"sep-1\", label: \"\" },\n  {\n    id: \"bullet-list\",\n    label: \"Bullet list\",\n    icon: <List />,\n    run: (ctx) => ctx.toggleLinePrefix(\"- \"),\n  },\n  {\n    id: \"numbered-list\",\n    label: \"Numbered list\",\n    icon: <ListOrdered />,\n    run: (ctx) => ctx.toggleLinePrefix(\"1. \"),\n  },\n  {\n    id: \"blockquote\",\n    label: \"Blockquote\",\n    icon: <Quote />,\n    run: (ctx) => ctx.toggleLinePrefix(\"> \"),\n  },\n  {\n    id: \"heading\",\n    label: \"Heading\",\n    icon: <Heading />,\n    run: (ctx) => cycleHeading(ctx.view),\n  },\n];\n",
      "type": "registry:component",
      "target": "components/markdown-editor/default-toolbar.tsx"
    },
    {
      "path": "src/registry/components/forms/markdown-editor/hooks/use-codemirror.ts",
      "content": "import { useEffect, useRef, useState } from \"react\";\nimport { EditorState, type Extension } from \"@codemirror/state\";\nimport {\n  EditorView,\n  drawSelection,\n  highlightActiveLine,\n  keymap,\n} from \"@codemirror/view\";\nimport { defaultKeymap, history, historyKeymap, indentWithTab } from \"@codemirror/commands\";\nimport { search, searchKeymap, highlightSelectionMatches } from \"@codemirror/search\";\nimport {\n  autocompletion,\n  closeBrackets,\n  closeBracketsKeymap,\n  completionKeymap,\n} from \"@codemirror/autocomplete\";\nimport { markdown, markdownLanguage } from \"@codemirror/lang-markdown\";\nimport { GFM } from \"@lezer/markdown\";\n\nimport { markdownEditorTheme } from \"../lib/extensions/theme\";\nimport {\n  buildCandidatesMap,\n  setCandidatesEffect,\n  wikilinkExtension,\n} from \"../lib/extensions/wikilink\";\nimport { createWikilinkCompletionSource } from \"../lib/extensions/wikilink-autocomplete\";\nimport { saveKeymap } from \"../lib/extensions/save-keymap\";\nimport { markdownKeymap } from \"../lib/extensions/markdown-keymap\";\nimport { readOnlyCompartment, userExtensionsCompartment } from \"../lib/extensions/compartments\";\nimport { SyncAnnotation } from \"../lib/sync-annotation\";\nimport type { KindMeta, WikilinkCandidate } from \"../types\";\nimport type { RefObject } from \"react\";\n\ninterface UseCodeMirrorOpts {\n  value: string;\n  readOnly: boolean;\n  wikilinkCandidates: ReadonlyArray<WikilinkCandidate> | undefined;\n  userExtensions: ReadonlyArray<Extension>;\n  onChangeRef: RefObject<(value: string) => void>;\n  onSaveRef: RefObject<((value: string) => void) | undefined>;\n  kindsRef: RefObject<Record<string, KindMeta> | undefined>;\n  // Notified on selection-set or doc-change CM6 updates so dependent surfaces\n  // (toolbar active state) can re-render without reconfiguring extensions.\n  onRelevantUpdateRef: RefObject<(() => void) | undefined>;\n  ariaLabel: string | undefined;\n}\n\ninterface UseCodeMirrorResult {\n  // React-state view — null until CM6 mounts; safe to read during render.\n  view: EditorView | null;\n  setContainer: (node: HTMLDivElement | null) => void;\n}\n\nexport function useCodeMirror(opts: UseCodeMirrorOpts): UseCodeMirrorResult {\n  const {\n    value,\n    readOnly,\n    wikilinkCandidates,\n    userExtensions,\n    onChangeRef,\n    onSaveRef,\n    kindsRef,\n    onRelevantUpdateRef,\n    ariaLabel,\n  } = opts;\n\n  const [view, setView] = useState<EditorView | null>(null);\n  const lastSyncedValueRef = useRef<string>(value);\n  const [container, setContainer] = useState<HTMLDivElement | null>(null);\n\n  // Mount-time prop snapshot via refs — read by mount effect; updated post-commit so\n  // the snapshot stays fresh if a parent re-renders between hook call and mount.\n  const valueAtMountRef = useRef(value);\n  const readOnlyAtMountRef = useRef(readOnly);\n  const candidatesAtMountRef = useRef(wikilinkCandidates);\n  const userExtensionsAtMountRef = useRef(userExtensions);\n  const ariaLabelAtMountRef = useRef(ariaLabel);\n\n  useEffect(() => {\n    valueAtMountRef.current = value;\n    readOnlyAtMountRef.current = readOnly;\n    candidatesAtMountRef.current = wikilinkCandidates;\n    userExtensionsAtMountRef.current = userExtensions;\n    ariaLabelAtMountRef.current = ariaLabel;\n  });\n\n  // Mount effect — keyed only on container; subsequent prop changes flow through\n  // the sync effects below. Mount-time prop reads come from the refs above.\n  useEffect(() => {\n    if (!container) return;\n\n    const initialDoc = valueAtMountRef.current;\n    const initialReadOnly = readOnlyAtMountRef.current;\n    const initialCandidates = candidatesAtMountRef.current;\n    const initialUserExtensions = userExtensionsAtMountRef.current;\n    const initialAriaLabel = ariaLabelAtMountRef.current;\n\n    const extensions: Extension[] = [\n      EditorView.lineWrapping,\n      EditorState.tabSize.of(2),\n      drawSelection(),\n      highlightActiveLine(),\n      highlightSelectionMatches(),\n      closeBrackets(),\n\n      markdown({ base: markdownLanguage, extensions: [GFM], addKeymap: true }),\n\n      history(),\n      search({ top: true }),\n\n      ...wikilinkExtension,\n\n      autocompletion({\n        override: [createWikilinkCompletionSource(kindsRef)],\n      }),\n\n      markdownEditorTheme,\n\n      // Keymap — most-specific first; each binding's `run: () => true` stops further keymaps.\n      keymap.of([\n        ...saveKeymap(onSaveRef),\n        ...markdownKeymap(),\n        ...closeBracketsKeymap,\n        ...defaultKeymap,\n        ...historyKeymap,\n        ...searchKeymap,\n        ...completionKeymap,\n        indentWithTab,\n      ]),\n\n      readOnlyCompartment.of(EditorState.readOnly.of(initialReadOnly)),\n\n      // Echo-guard updateListener (Q-P9) — skip onChange when transaction carries SyncAnnotation.\n      EditorView.updateListener.of((update) => {\n        if (\n          update.docChanged &&\n          !update.transactions.some((t) => t.annotation(SyncAnnotation))\n        ) {\n          onChangeRef.current(update.state.doc.toString());\n        }\n        if (update.selectionSet || update.docChanged) {\n          onRelevantUpdateRef.current?.();\n        }\n      }),\n\n      EditorView.contentAttributes.of(\n        initialAriaLabel ? { \"aria-label\": initialAriaLabel } : {},\n      ),\n\n      // User extensions LAST — earlier-in-array = HIGHER CM6 default precedence (Q-P8 lock).\n      // Our defaults win conflicts; users escalate via Prec.high(...).\n      userExtensionsCompartment.of([...initialUserExtensions]),\n    ];\n\n    const editorView = new EditorView({\n      state: EditorState.create({ doc: initialDoc, extensions }),\n      parent: container,\n    });\n\n    // Seed initial candidates via dispatch (StateField starts empty).\n    if (initialCandidates && initialCandidates.length > 0) {\n      editorView.dispatch({\n        effects: setCandidatesEffect.of(buildCandidatesMap(initialCandidates)),\n      });\n    }\n\n    setView(editorView);\n    lastSyncedValueRef.current = initialDoc;\n\n    return () => {\n      editorView.destroy();\n      setView(null);\n    };\n    // Mount only on container change. All prop changes flow through sync effects below.\n  }, [container, kindsRef, onChangeRef, onSaveRef, onRelevantUpdateRef]);\n\n  // Value sync (Q-P9 echo-guarded).\n  useEffect(() => {\n    if (!view) return;\n    if (\n      value !== lastSyncedValueRef.current &&\n      value !== view.state.doc.toString()\n    ) {\n      view.dispatch({\n        changes: { from: 0, to: view.state.doc.length, insert: value },\n        annotations: SyncAnnotation.of(true),\n      });\n    }\n    lastSyncedValueRef.current = value;\n  }, [view, value]);\n\n  // ReadOnly sync.\n  useEffect(() => {\n    view?.dispatch({\n      effects: readOnlyCompartment.reconfigure(EditorState.readOnly.of(readOnly)),\n    });\n  }, [view, readOnly]);\n\n  // wikilinkCandidates sync (validate-pass refinement #2).\n  useEffect(() => {\n    if (!view) return;\n    view.dispatch({\n      effects: setCandidatesEffect.of(buildCandidatesMap(wikilinkCandidates ?? [])),\n    });\n  }, [view, wikilinkCandidates]);\n\n  // User extensions sync (full reconfigure on reference change).\n  useEffect(() => {\n    view?.dispatch({\n      effects: userExtensionsCompartment.reconfigure([...userExtensions]),\n    });\n  }, [view, userExtensions]);\n\n  return { view, setContainer };\n}\n",
      "type": "registry:component",
      "target": "components/markdown-editor/hooks/use-codemirror.ts"
    },
    {
      "path": "src/registry/components/forms/markdown-editor/hooks/use-imperative-handle.ts",
      "content": "import { useImperativeHandle, useMemo, type Ref } from \"react\";\nimport { undo as cmUndo, redo as cmRedo } from \"@codemirror/commands\";\nimport type { EditorView } from \"@codemirror/view\";\nimport { insertText as insertTextAction } from \"../lib/toolbar-actions\";\nimport type { MarkdownEditorHandle } from \"../types\";\n\ninterface UseHandleOpts {\n  ref: Ref<MarkdownEditorHandle> | undefined;\n  view: EditorView | null;\n}\n\nconst DEV = process.env.NODE_ENV !== \"production\";\nlet warnedGetViewBeforeMount = false;\n\nexport function useMarkdownEditorHandle({ ref, view }: UseHandleOpts): void {\n  const handle = useMemo<MarkdownEditorHandle>(\n    () => ({\n      focus() {\n        view?.focus();\n      },\n      undo() {\n        if (!view) return;\n        cmUndo(view);\n      },\n      redo() {\n        if (!view) return;\n        cmRedo(view);\n      },\n      insertText(text: string) {\n        if (!view) return;\n        insertTextAction(view, text);\n      },\n      getSelection() {\n        if (!view) return { from: 0, to: 0, text: \"\" };\n        const { from, to } = view.state.selection.main;\n        return { from, to, text: view.state.sliceDoc(from, to) };\n      },\n      getValue() {\n        return view?.state.doc.toString() ?? \"\";\n      },\n      getView() {\n        if (!view && DEV && !warnedGetViewBeforeMount) {\n          warnedGetViewBeforeMount = true;\n          console.warn(\n            \"[markdown-editor] getView() called before mount or after unmount; returned null.\",\n          );\n        }\n        return view;\n      },\n    }),\n    [view],\n  );\n\n  useImperativeHandle(ref, () => handle, [handle]);\n}\n",
      "type": "registry:component",
      "target": "components/markdown-editor/hooks/use-imperative-handle.ts"
    },
    {
      "path": "src/registry/components/forms/markdown-editor/hooks/use-toolbar-state.ts",
      "content": "import { useState, useSyncExternalStore } from \"react\";\n\n// External store: tracks a CM6-update counter so toolbar buttons re-render only when\n// CM6 fires a relevant update (selection-change or doc-change), not on every keystroke\n// at the React level (plan §4.4).\nexport interface ToolbarStore {\n  subscribe: (cb: () => void) => () => void;\n  getSnapshot: () => number;\n  notify: () => void;\n}\n\nexport function createToolbarStore(): ToolbarStore {\n  let version = 0;\n  const listeners = new Set<() => void>();\n  return {\n    subscribe(cb) {\n      listeners.add(cb);\n      return () => {\n        listeners.delete(cb);\n      };\n    },\n    getSnapshot() {\n      return version;\n    },\n    notify() {\n      version++;\n      listeners.forEach((cb) => cb());\n    },\n  };\n}\n\nexport function useToolbarStore(): ToolbarStore {\n  const [store] = useState(createToolbarStore);\n  return store;\n}\n\nexport function useToolbarVersion(store: ToolbarStore): number {\n  return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);\n}\n",
      "type": "registry:component",
      "target": "components/markdown-editor/hooks/use-toolbar-state.ts"
    },
    {
      "path": "src/registry/components/forms/markdown-editor/hooks/use-view-mode.ts",
      "content": "import { useCallback, useState } from \"react\";\nimport type { ViewMode } from \"../types\";\n\ninterface UseViewModeOpts {\n  view: ViewMode | undefined;\n  onViewChange: ((view: ViewMode) => void) | undefined;\n  initialView: ViewMode;\n  showPreviewToggle: boolean;\n}\n\ninterface UseViewModeResult {\n  viewValue: ViewMode;\n  handleViewChange: (next: ViewMode) => void;\n  toggleHidden: boolean;\n}\n\n// Controlled-or-uncontrolled view-mode dispatch (plan §4.3).\n// When showPreviewToggle is false, view is locked to initialView (toggle is hidden too).\nexport function useViewMode(opts: UseViewModeOpts): UseViewModeResult {\n  const { view, onViewChange, initialView, showPreviewToggle } = opts;\n  const isControlled = view !== undefined;\n  const [internalView, setInternalView] = useState<ViewMode>(initialView);\n\n  const lockedView = !showPreviewToggle ? initialView : null;\n  const viewValue = lockedView ?? (isControlled ? view : internalView);\n\n  const handleViewChange = useCallback(\n    (next: ViewMode) => {\n      if (lockedView) return;\n      if (!isControlled) setInternalView(next);\n      onViewChange?.(next);\n    },\n    [isControlled, lockedView, onViewChange],\n  );\n\n  return { viewValue, handleViewChange, toggleHidden: !showPreviewToggle };\n}\n",
      "type": "registry:component",
      "target": "components/markdown-editor/hooks/use-view-mode.ts"
    },
    {
      "path": "src/registry/components/forms/markdown-editor/lib/parse-markdown.ts",
      "content": "import { Marked, type Tokens } from \"marked\";\nimport type { KindMeta, WikilinkCandidate } from \"../types\";\n\ninterface WikilinkToken extends Tokens.Generic {\n  type: \"wikilink\";\n  raw: string;\n  label: string;\n  alias: string | undefined;\n}\n\nconst wikilinkExtension = {\n  extensions: [\n    {\n      name: \"wikilink\",\n      level: \"inline\" as const,\n      start: (src: string) => src.indexOf(\"[[\"),\n      tokenizer(src: string): WikilinkToken | undefined {\n        const match = src.match(/^\\[\\[([^[\\]\\n|]+?)(?:\\|([^[\\]\\n]+?))?\\]\\]/);\n        if (!match) return undefined;\n        return {\n          type: \"wikilink\",\n          raw: match[0],\n          label: match[1].trim(),\n          alias: match[2]?.trim(),\n        };\n      },\n      renderer(token: WikilinkToken): string {\n        const display = token.alias ?? token.label;\n        return `<span class=\"wikilink\" data-wikilink-target=\"${escapeHtml(token.label)}\">${escapeHtml(display)}</span>`;\n      },\n    },\n  ],\n};\n\n// ─── Sanitization (v0.1.2, deep-review finding 5.1) ─────────────────────────\n// `marked` passes raw HTML through by default — with backend-stored multi-author\n// markdown (the wikilink use case) that is stored XSS in consumer apps. Two\n// per-instance defenses, no added dependency:\n//   1. Raw HTML tokens (block `html` + inline tags) render as ESCAPED text —\n//      `<img onerror=…>` displays literally instead of executing. This also\n//      prevents hand-forged `.wikilink` spans from reaching the post-process\n//      regex below.\n//   2. Link hrefs are scheme-filtered — `javascript:` / `data:` / `vbscript:`\n//      and every other non-allowlisted scheme are stripped (the link renders as\n//      plain text); `http:` / `https:` / `mailto:`, `#fragments`, and\n//      relative/absolute paths pass through. Attribute output is entity-escaped,\n//      so `&colon;`-style entity tricks are neutralized too.\n// Wikilink rendering is unaffected (its renderer already escapes label+target).\n\nconst SCHEME_RE = /^([a-zA-Z][a-zA-Z0-9+.-]*):/;\nconst SAFE_SCHEME_RE = /^(?:https?|mailto)$/i;\n\nfunction sanitizeHref(href: string): string | null {\n  // Strip control chars + whitespace browsers ignore when parsing the scheme\n  // (\"jav\\tascript:\" tricks) before checking, but return the original href.\n  const cleaned = href.replace(/[\\u0000-\\u0020\\u007f]/g, \"\");\n  const schemeMatch = SCHEME_RE.exec(cleaned);\n  if (!schemeMatch) return href; // relative path / #fragment / query — no scheme\n  return SAFE_SCHEME_RE.test(schemeMatch[1]) ? href : null;\n}\n\n// Per-instance Marked (Q-P1 lock + validate-pass refinement #1) — avoids global\n// `marked.use(...)` mutation that would pollute every other consumer of `marked`\n// in the same bundle (e.g., the docs-site README parser).\nconst customMarked = new Marked();\ncustomMarked.use(\n  { gfm: true, breaks: false },\n  {\n    renderer: {\n      // Raw HTML (block + inline) → escaped literal text, never markup.\n      html(token: Tokens.HTML | Tokens.Tag): string {\n        return escapeHtml(token.text);\n      },\n      // Links → scheme-filtered href; disallowed schemes drop the anchor and\n      // keep the (inline-parsed) label text.\n      link(token: Tokens.Link): string {\n        const text = this.parser.parseInline(token.tokens);\n        const href = sanitizeHref(token.href);\n        if (href === null) return text;\n        const title = token.title ? ` title=\"${escapeHtml(token.title)}\"` : \"\";\n        return `<a href=\"${escapeHtml(href)}\"${title}>${text}</a>`;\n      },\n    },\n  },\n  wikilinkExtension,\n);\n\ninterface ParseOpts {\n  wikilinkCandidates?: ReadonlyArray<WikilinkCandidate>;\n  // kinds is part of the parse opts for forward-compat with kind-aware preview rendering;\n  // unused in v0.1 (autocomplete renders kind badges, not preview).\n  kinds?: Record<string, KindMeta>;\n  hasClickHandler: boolean;\n}\n\nconst WIKILINK_SPAN_REGEX = /<span class=\"wikilink\" data-wikilink-target=\"([^\"]+)\">/g;\n\nexport function parseMarkdown(source: string, opts: ParseOpts): string {\n  const html = customMarked.parse(source, { async: false }) as string;\n\n  // Post-process: mark broken wikilinks; conditionally add role+tabindex when interactive\n  // (validate-pass refinement #4 — only marked interactive when host wires onWikilinkClick).\n  const interactive = opts.hasClickHandler;\n\n  if (!opts.wikilinkCandidates) {\n    return interactive\n      ? html.replace(\n          WIKILINK_SPAN_REGEX,\n          (_, target) =>\n            `<span class=\"wikilink\" role=\"link\" tabindex=\"0\" data-wikilink-target=\"${target}\">`,\n        )\n      : html;\n  }\n\n  const labelSet = new Set(opts.wikilinkCandidates.map((c) => c.label.toLowerCase().trim()));\n\n  return html.replace(WIKILINK_SPAN_REGEX, (_, target) => {\n    const resolved = labelSet.has(String(target).toLowerCase().trim());\n    const classes = `wikilink${resolved ? \"\" : \" wikilink-broken\"}`;\n    const roleAttr = interactive ? ' role=\"link\" tabindex=\"0\"' : \"\";\n    return `<span class=\"${classes}\"${roleAttr} data-wikilink-target=\"${target}\">`;\n  });\n}\n\nfunction escapeHtml(s: string): string {\n  return s.replace(/[&<>\"']/g, (ch) => {\n    switch (ch) {\n      case \"&\":\n        return \"&amp;\";\n      case \"<\":\n        return \"&lt;\";\n      case \">\":\n        return \"&gt;\";\n      case '\"':\n        return \"&quot;\";\n      case \"'\":\n        return \"&#39;\";\n      default:\n        return ch;\n    }\n  });\n}\n",
      "type": "registry:component",
      "target": "components/markdown-editor/lib/parse-markdown.ts"
    },
    {
      "path": "src/registry/components/forms/markdown-editor/lib/sync-annotation.ts",
      "content": "import { Annotation } from \"@codemirror/state\";\n\n// Marks transactions originating from React value-prop sync so the updateListener\n// skips firing onChange and avoids the value-sync echo (Q-P9 lock).\nexport const SyncAnnotation = Annotation.define<boolean>();\n",
      "type": "registry:component",
      "target": "components/markdown-editor/lib/sync-annotation.ts"
    },
    {
      "path": "src/registry/components/forms/markdown-editor/lib/toolbar-actions.ts",
      "content": "import type { EditorView } from \"@codemirror/view\";\n\nexport function wrapSelection(view: EditorView, before: string, after: string = before): void {\n  const { from, to } = view.state.selection.main;\n  const selectedText = view.state.sliceDoc(from, to);\n  view.dispatch({\n    changes: { from, to, insert: `${before}${selectedText}${after}` },\n    selection:\n      from === to\n        ? { anchor: from + before.length }\n        : { anchor: from + before.length, head: from + before.length + selectedText.length },\n  });\n  view.focus();\n}\n\nexport function toggleLinePrefix(view: EditorView, prefix: string): void {\n  const { from, to } = view.state.selection.main;\n  const startLine = view.state.doc.lineAt(from).number;\n  const endLine = view.state.doc.lineAt(to).number;\n\n  const lines: { from: number; to: number; text: string }[] = [];\n  for (let n = startLine; n <= endLine; n++) {\n    const line = view.state.doc.line(n);\n    lines.push({ from: line.from, to: line.to, text: line.text });\n  }\n\n  const allHavePrefix = lines.every((l) => l.text.startsWith(prefix));\n  const changes = lines.map((l) =>\n    allHavePrefix\n      ? { from: l.from, to: l.from + prefix.length, insert: \"\" }\n      : { from: l.from, insert: prefix },\n  );\n\n  view.dispatch({ changes });\n  view.focus();\n}\n\nexport function insertText(view: EditorView, text: string): void {\n  const { from, to } = view.state.selection.main;\n  view.dispatch({\n    changes: { from, to, insert: text },\n    selection: { anchor: from + text.length },\n  });\n  view.focus();\n}\n\n// Heading cycle: H1 → H2 → H3 → no-prefix → H1 (§13.5 #8).\nconst HEADING_PREFIXES = [\"# \", \"## \", \"### \"] as const;\n\nexport function cycleHeading(view: EditorView): void {\n  const { from } = view.state.selection.main;\n  const line = view.state.doc.lineAt(from);\n  const text = line.text;\n\n  let currentIdx = -1;\n  for (let i = 0; i < HEADING_PREFIXES.length; i++) {\n    if (text.startsWith(HEADING_PREFIXES[i])) {\n      currentIdx = i;\n      break;\n    }\n  }\n\n  const stripLen = currentIdx >= 0 ? HEADING_PREFIXES[currentIdx].length : 0;\n  const nextIdx = currentIdx + 1;\n  const nextPrefix = nextIdx < HEADING_PREFIXES.length ? HEADING_PREFIXES[nextIdx] : \"\";\n\n  view.dispatch({\n    changes: { from: line.from, to: line.from + stripLen, insert: nextPrefix },\n  });\n  view.focus();\n}\n\n// Detect if the inline mark (e.g. \"**\" for bold) wraps the current cursor or selection.\nexport function isInlineMarkActive(view: EditorView, mark: string): boolean {\n  const { from, to } = view.state.selection.main;\n  const text = view.state.doc.toString();\n  const before = text.slice(Math.max(0, from - mark.length), from);\n  const after = text.slice(to, to + mark.length);\n  return before === mark && after === mark;\n}\n",
      "type": "registry:component",
      "target": "components/markdown-editor/lib/toolbar-actions.ts"
    },
    {
      "path": "src/registry/components/forms/markdown-editor/lib/wikilink-grammar.ts",
      "content": "// Canonical wikilink pattern (Q-P2 lock):\n// - [[label]]\n// - [[label|alias]]\n// - NOT image embeds (![[...]]) — caller must check isImageEmbed\n// - NO nested square brackets\n// - NO #anchor (deferred to v0.2)\nexport const WIKILINK_PATTERN = \"\\\\[\\\\[([^\\\\[\\\\]\\\\n|]+?)(?:\\\\|([^\\\\[\\\\]\\\\n]+?))?\\\\]\\\\]\";\n\nexport function makeWikilinkRegex(): RegExp {\n  return new RegExp(WIKILINK_PATTERN, \"g\");\n}\n\nexport interface WikilinkMatch {\n  full: string;\n  label: string;\n  alias: string | undefined;\n  start: number;\n  end: number;\n}\n\nexport function findWikilinks(text: string): WikilinkMatch[] {\n  const matches: WikilinkMatch[] = [];\n  for (const m of text.matchAll(makeWikilinkRegex())) {\n    if (m.index === undefined) continue;\n    if (isImageEmbed(text, m.index)) continue;\n    matches.push({\n      full: m[0],\n      label: m[1].trim(),\n      alias: m[2]?.trim(),\n      start: m.index,\n      end: m.index + m[0].length,\n    });\n  }\n  return matches;\n}\n\nexport function isImageEmbed(text: string, matchStart: number): boolean {\n  return matchStart > 0 && text.charAt(matchStart - 1) === \"!\";\n}\n",
      "type": "registry:component",
      "target": "components/markdown-editor/lib/wikilink-grammar.ts"
    },
    {
      "path": "src/registry/components/forms/markdown-editor/lib/extensions/compartments.ts",
      "content": "import { Compartment } from \"@codemirror/state\";\n\nexport const readOnlyCompartment = new Compartment();\nexport const userExtensionsCompartment = new Compartment();\n",
      "type": "registry:component",
      "target": "components/markdown-editor/lib/extensions/compartments.ts"
    },
    {
      "path": "src/registry/components/forms/markdown-editor/lib/extensions/markdown-keymap.ts",
      "content": "import type { EditorView, KeyBinding } from \"@codemirror/view\";\n\n// Standard markdown keymap (description Q2; plan §5.4).\n// Mod-B / Mod-I / Mod-E / Mod-K wrap selection; Mod-Shift-period toggles blockquote.\n\nfunction wrap(view: EditorView, before: string, after: string = before): boolean {\n  const { from, to } = view.state.selection.main;\n  const selectedText = view.state.sliceDoc(from, to);\n  view.dispatch({\n    changes: { from, to, insert: `${before}${selectedText}${after}` },\n    selection:\n      from === to\n        ? { anchor: from + before.length }\n        : { anchor: from + before.length, head: from + before.length + selectedText.length },\n  });\n  view.focus();\n  return true;\n}\n\nfunction wrapLink(view: EditorView): boolean {\n  // [[selection]](url) with cursor placed inside the (url) slot per §8.5 #1.\n  const { from, to } = view.state.selection.main;\n  const selectedText = view.state.sliceDoc(from, to);\n  const before = `[${selectedText}](`;\n  const after = \")\";\n  const cursor = from + before.length;\n  view.dispatch({\n    changes: { from, to, insert: `${before}${after}` },\n    selection: { anchor: cursor },\n  });\n  view.focus();\n  return true;\n}\n\nfunction toggleBlockquote(view: EditorView): boolean {\n  const prefix = \"> \";\n  const { from, to } = view.state.selection.main;\n  const startLine = view.state.doc.lineAt(from).number;\n  const endLine = view.state.doc.lineAt(to).number;\n  const lines: { from: number; to: number; text: string }[] = [];\n  for (let n = startLine; n <= endLine; n++) {\n    const line = view.state.doc.line(n);\n    lines.push({ from: line.from, to: line.to, text: line.text });\n  }\n  const allHavePrefix = lines.every((l) => l.text.startsWith(prefix));\n  const changes = lines.map((l) =>\n    allHavePrefix\n      ? { from: l.from, to: l.from + prefix.length, insert: \"\" }\n      : { from: l.from, insert: prefix },\n  );\n  view.dispatch({ changes });\n  view.focus();\n  return true;\n}\n\nexport function markdownKeymap(): KeyBinding[] {\n  return [\n    { key: \"Mod-b\", run: (view) => wrap(view, \"**\") },\n    { key: \"Mod-i\", run: (view) => wrap(view, \"*\") },\n    { key: \"Mod-e\", run: (view) => wrap(view, \"`\") },\n    { key: \"Mod-k\", run: (view) => wrapLink(view) },\n    { key: \"Mod-Shift-.\", run: (view) => toggleBlockquote(view) },\n  ];\n}\n",
      "type": "registry:component",
      "target": "components/markdown-editor/lib/extensions/markdown-keymap.ts"
    },
    {
      "path": "src/registry/components/forms/markdown-editor/lib/extensions/save-keymap.ts",
      "content": "import type { KeyBinding } from \"@codemirror/view\";\nimport type { RefObject } from \"react\";\n\n// Cmd/Ctrl+S → onSave (description §8.5 #4 + §8.5 #5).\n// preventDefault is gated on whether onSave is supplied:\n//   - onSaveRef.current set → run returns true; CM6 marks transaction handled and prevents browser default.\n//   - onSaveRef.current undefined → run returns false; browser's native save dialog fires (no surprise).\n// Payload is the CURRENT CM6 doc, NOT the React `value` prop (avoids stale-by-React-batching).\nexport function saveKeymap(\n  onSaveRef: RefObject<((value: string) => void) | undefined>,\n): KeyBinding[] {\n  return [\n    {\n      key: \"Mod-s\",\n      preventDefault: false,\n      run: (view) => {\n        const onSave = onSaveRef.current;\n        if (!onSave) return false;\n        onSave(view.state.doc.toString());\n        return true;\n      },\n    },\n  ];\n}\n",
      "type": "registry:component",
      "target": "components/markdown-editor/lib/extensions/save-keymap.ts"
    },
    {
      "path": "src/registry/components/forms/markdown-editor/lib/extensions/theme.ts",
      "content": "import { EditorView } from \"@codemirror/view\";\nimport { HighlightStyle, syntaxHighlighting } from \"@codemirror/language\";\nimport { tags as t } from \"@lezer/highlight\";\nimport type { Extension } from \"@codemirror/state\";\n\n// CM6 theme mapping editor surface to globals.css CSS variables (Q5 + §8.5 #5).\n// All colors flow through `var(--*)` so dark/light theme flips at the document\n// level update the editor without notifying CM6 — no remount.\nconst editorTheme = EditorView.theme({\n  \"&\": {\n    backgroundColor: \"var(--card)\",\n    color: \"var(--foreground)\",\n    fontFamily: \"var(--font-mono)\",\n    fontSize: \"0.875rem\",\n    lineHeight: \"1.65\",\n    height: \"100%\",\n  },\n  \".cm-content\": {\n    caretColor: \"var(--foreground)\",\n    padding: \"0.875rem 1rem\",\n  },\n  \".cm-scroller\": {\n    fontFamily: \"inherit\",\n    overflowY: \"auto\",\n  },\n  \".cm-cursor, .cm-dropCursor\": {\n    borderLeftColor: \"var(--foreground)\",\n    borderLeftWidth: \"1.5px\",\n  },\n  \"&.cm-focused\": {\n    outline: \"none\",\n  },\n  \"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, ::selection\": {\n    backgroundColor: \"color-mix(in oklch, var(--primary) 28%, transparent)\",\n  },\n  \".cm-activeLine\": {\n    backgroundColor: \"color-mix(in oklch, var(--muted) 55%, transparent)\",\n  },\n  \".cm-gutters\": {\n    backgroundColor: \"transparent\",\n    color: \"var(--muted-foreground)\",\n    border: \"none\",\n  },\n  \".cm-activeLineGutter\": {\n    backgroundColor: \"transparent\",\n    color: \"var(--foreground)\",\n  },\n  \".cm-selectionMatch\": {\n    backgroundColor: \"color-mix(in oklch, var(--primary) 18%, transparent)\",\n  },\n\n  // Wikilink decoration (Q-P5; styled per resolved/broken)\n  \".cm-wikilink\": {\n    color: \"var(--primary)\",\n    backgroundColor: \"color-mix(in oklch, var(--primary) 12%, transparent)\",\n    borderRadius: \"3px\",\n    padding: \"1px 2px\",\n  },\n  \".cm-wikilink-broken\": {\n    color: \"var(--destructive)\",\n    backgroundColor: \"color-mix(in oklch, var(--destructive) 10%, transparent)\",\n    textDecoration: \"underline dashed\",\n    textUnderlineOffset: \"3px\",\n  },\n\n  // Search panel (description §3 — out of scope to redesign; map colors only per §13.5 #11)\n  \".cm-panels\": {\n    backgroundColor: \"var(--popover)\",\n    color: \"var(--popover-foreground)\",\n    borderTop: \"1px solid var(--border)\",\n  },\n  \".cm-panels input\": {\n    backgroundColor: \"var(--background)\",\n    color: \"var(--foreground)\",\n    border: \"1px solid var(--border)\",\n    borderRadius: \"0.375rem\",\n    padding: \"0.25rem 0.5rem\",\n  },\n  \".cm-panels button\": {\n    backgroundColor: \"var(--secondary)\",\n    color: \"var(--secondary-foreground)\",\n    border: \"1px solid var(--border)\",\n    borderRadius: \"0.375rem\",\n    padding: \"0.125rem 0.5rem\",\n    marginLeft: \"0.25rem\",\n  },\n\n  // Autocomplete popup\n  \".cm-tooltip.cm-tooltip-autocomplete\": {\n    backgroundColor: \"var(--popover)\",\n    color: \"var(--popover-foreground)\",\n    border: \"1px solid var(--border)\",\n    borderRadius: \"0.5rem\",\n    boxShadow: \"0 8px 24px -8px rgb(0 0 0 / 0.18)\",\n    fontFamily: \"var(--font-sans)\",\n  },\n  \".cm-tooltip-autocomplete > ul\": {\n    fontFamily: \"var(--font-sans)\",\n    fontSize: \"0.8125rem\",\n    maxHeight: \"16rem\",\n  },\n  \".cm-tooltip-autocomplete > ul > li\": {\n    padding: \"0.25rem 0.625rem\",\n  },\n  \".cm-tooltip-autocomplete > ul > li[aria-selected]\": {\n    backgroundColor: \"var(--accent)\",\n    color: \"var(--accent-foreground)\",\n  },\n  \".cm-completionInfo\": {\n    backgroundColor: \"var(--popover)\",\n    color: \"var(--popover-foreground)\",\n    border: \"1px solid var(--border)\",\n    borderRadius: \"0.5rem\",\n    padding: \"0.5rem 0.75rem\",\n    fontFamily: \"var(--font-sans)\",\n  },\n});\n\n// Syntax highlighting for markdown tokens — maps lezer tags to design-token colors\nconst markdownHighlight = HighlightStyle.define([\n  { tag: [t.heading1, t.heading2, t.heading3, t.heading4, t.heading5, t.heading6], color: \"var(--foreground)\", fontWeight: \"600\" },\n  { tag: t.strong, fontWeight: \"700\" },\n  { tag: t.emphasis, fontStyle: \"italic\" },\n  { tag: t.strikethrough, textDecoration: \"line-through\", color: \"var(--muted-foreground)\" },\n  { tag: t.link, color: \"var(--primary)\", textDecoration: \"underline\" },\n  { tag: t.url, color: \"var(--primary)\" },\n  { tag: t.monospace, color: \"var(--foreground)\", backgroundColor: \"color-mix(in oklch, var(--muted) 70%, transparent)\", borderRadius: \"3px\", padding: \"0 3px\" },\n  { tag: t.quote, color: \"var(--muted-foreground)\", fontStyle: \"italic\" },\n  { tag: t.list, color: \"var(--muted-foreground)\" },\n  { tag: t.atom, color: \"var(--primary)\" },\n  { tag: t.processingInstruction, color: \"var(--muted-foreground)\" },\n  { tag: t.contentSeparator, color: \"var(--border)\" },\n]);\n\nexport const markdownEditorTheme: Extension = [\n  editorTheme,\n  syntaxHighlighting(markdownHighlight, { fallback: true }),\n];\n",
      "type": "registry:component",
      "target": "components/markdown-editor/lib/extensions/theme.ts"
    },
    {
      "path": "src/registry/components/forms/markdown-editor/lib/extensions/wikilink-autocomplete.ts",
      "content": "import {\n  type CompletionContext,\n  type CompletionResult,\n  type CompletionSource,\n  closeCompletion,\n} from \"@codemirror/autocomplete\";\nimport type { EditorView } from \"@codemirror/view\";\nimport type { RefObject } from \"react\";\nimport { candidatesField } from \"./wikilink\";\nimport type { KindMeta, WikilinkCandidate } from \"../../types\";\n\n// Cap visible results to 50 (Q-P3 lock); overflow gets a sentinel \"+ N more (refine search)\" row.\nconst RESULT_CAP = 50;\n\nfunction filterCandidates(\n  candidates: ReadonlyArray<WikilinkCandidate>,\n  query: string,\n): WikilinkCandidate[] {\n  if (!query) return candidates.slice();\n  const needle = query.toLowerCase();\n  return candidates.filter((c) => c.label.toLowerCase().includes(needle));\n}\n\nfunction renderInfoPanel(\n  candidate: WikilinkCandidate,\n  kinds: Record<string, KindMeta> | undefined,\n): HTMLElement {\n  const root = document.createElement(\"div\");\n  root.className = \"markdown-editor-completion-info\";\n  root.style.display = \"flex\";\n  root.style.flexDirection = \"column\";\n  root.style.gap = \"4px\";\n\n  if (candidate.kind && kinds?.[candidate.kind]) {\n    const meta = kinds[candidate.kind];\n    const badge = document.createElement(\"span\");\n    badge.textContent = meta.label;\n    badge.style.alignSelf = \"flex-start\";\n    badge.style.fontSize = \"0.6875rem\";\n    badge.style.fontWeight = \"600\";\n    badge.style.padding = \"1px 6px\";\n    badge.style.borderRadius = \"999px\";\n    badge.style.color = meta.color ?? \"var(--foreground)\";\n    badge.style.backgroundColor = meta.color\n      ? `color-mix(in oklch, ${meta.color} 14%, transparent)`\n      : \"var(--muted)\";\n    badge.style.border = `1px solid ${meta.color ?? \"var(--border)\"}`;\n    root.appendChild(badge);\n  }\n\n  const label = document.createElement(\"div\");\n  label.textContent = candidate.label;\n  label.style.fontWeight = \"500\";\n  root.appendChild(label);\n\n  if (candidate.alias) {\n    const alias = document.createElement(\"div\");\n    alias.textContent = `alias: ${candidate.alias}`;\n    alias.style.fontSize = \"0.75rem\";\n    alias.style.color = \"var(--muted-foreground)\";\n    root.appendChild(alias);\n  }\n\n  return root;\n}\n\n// Factory: returns a completion source closed over a kindsRef (read lazily so kinds prop\n// updates flow through without reconfiguring the autocomplete extension).\nexport function createWikilinkCompletionSource(\n  kindsRef: RefObject<Record<string, KindMeta> | undefined>,\n): CompletionSource {\n  return (context: CompletionContext): CompletionResult | null => {\n    // Trigger: caret is somewhere inside an open wikilink token (anchored, NOT global).\n    const before = context.matchBefore(/\\[\\[([^[\\]\\n|]*)$/);\n    if (!before) return null;\n    if (before.from === before.to && !context.explicit) return null;\n\n    const query = before.text.slice(2);\n    const candidatesMap = context.state.field(candidatesField, false);\n    if (!candidatesMap) return null;\n\n    const candidates = Array.from(candidatesMap.values());\n    const filtered = filterCandidates(candidates, query);\n    const capped = filtered.slice(0, RESULT_CAP);\n    const overflow = filtered.length > RESULT_CAP ? filtered.length - RESULT_CAP : 0;\n\n    return {\n      from: before.from + 2,\n      to: context.pos,\n      options: [\n        ...capped.map((c) => ({\n          label: c.label,\n          apply: (view: EditorView, _completion: unknown, from: number, to: number) => {\n            const insert = c.alias ? `${c.label}|${c.alias}` : c.label;\n            view.dispatch({\n              changes: { from, to, insert: `${insert}]]` },\n              selection: { anchor: from + insert.length + 2 },\n            });\n          },\n          info: () => renderInfoPanel(c, kindsRef.current),\n        })),\n        ...(overflow > 0\n          ? [\n              {\n                label: `…and ${overflow} more (refine search)`,\n                apply: (view: EditorView) => closeCompletion(view),\n                type: \"text\" as const,\n                boost: -100,\n              },\n            ]\n          : []),\n      ],\n      validFor: /^[^[\\]\\n|]*$/,\n    };\n  };\n}\n",
      "type": "registry:component",
      "target": "components/markdown-editor/lib/extensions/wikilink-autocomplete.ts"
    },
    {
      "path": "src/registry/components/forms/markdown-editor/lib/extensions/wikilink.ts",
      "content": "import { StateEffect, StateField, type Extension } from \"@codemirror/state\";\nimport {\n  Decoration,\n  type DecorationSet,\n  EditorView,\n  MatchDecorator,\n  ViewPlugin,\n  type ViewUpdate,\n} from \"@codemirror/view\";\nimport { isImageEmbed, makeWikilinkRegex } from \"../wikilink-grammar\";\nimport type { WikilinkCandidate } from \"../../types\";\n\n// Effect to update candidates at runtime (validate-pass refinement #2 — Q-P5 lock).\nexport const setCandidatesEffect = StateEffect.define<Map<string, WikilinkCandidate>>();\n\n// StateField holding the current candidates Map (lowercased+trimmed labels → candidate).\nexport const candidatesField = StateField.define<Map<string, WikilinkCandidate>>({\n  create: () => new Map(),\n  update: (value, tr) => {\n    for (const effect of tr.effects) {\n      if (effect.is(setCandidatesEffect)) return effect.value;\n    }\n    return value;\n  },\n});\n\nexport function buildCandidatesMap(\n  candidates: ReadonlyArray<WikilinkCandidate>,\n): Map<string, WikilinkCandidate> {\n  return new Map(candidates.map((c) => [c.label.toLowerCase().trim(), c]));\n}\n\n// ViewPlugin: reads candidates from field; rebuilds decorations on doc OR field change.\nconst wikilinkPlugin = ViewPlugin.fromClass(\n  class {\n    decorations: DecorationSet;\n    matcher: MatchDecorator;\n\n    constructor(view: EditorView) {\n      this.matcher = new MatchDecorator({\n        regexp: makeWikilinkRegex(),\n        decoration: (match, view, pos) => {\n          const text = view.state.doc.toString();\n          if (isImageEmbed(text, pos)) return null;\n          const label = match[1].trim();\n          const candidates = view.state.field(candidatesField, false) ?? new Map();\n          // candidates may be empty when host doesn't supply wikilinkCandidates;\n          // treat all as broken in that case (visible signal that nothing resolves).\n          const resolved = candidates.size > 0 && candidates.has(label.toLowerCase());\n          return Decoration.mark({\n            class: resolved ? \"cm-wikilink\" : \"cm-wikilink cm-wikilink-broken\",\n          });\n        },\n      });\n      this.decorations = this.matcher.createDeco(view);\n    }\n\n    update(update: ViewUpdate) {\n      const oldCandidates = update.startState.field(candidatesField, false);\n      const newCandidates = update.state.field(candidatesField, false);\n      const candidatesChanged = oldCandidates !== newCandidates;\n      if (update.docChanged || candidatesChanged) {\n        this.decorations = candidatesChanged\n          ? this.matcher.createDeco(update.view)\n          : this.matcher.updateDeco(update, this.decorations);\n      }\n    }\n  },\n  {\n    decorations: (v) => v.decorations,\n  },\n);\n\nexport const wikilinkExtension: readonly Extension[] = [candidatesField, wikilinkPlugin];\n",
      "type": "registry:component",
      "target": "components/markdown-editor/lib/extensions/wikilink.ts"
    },
    {
      "path": "src/registry/components/forms/markdown-editor/parts/editor-pane.tsx",
      "content": "import type { CSSProperties } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ninterface EditorPaneProps {\n  setContainer: (node: HTMLDivElement | null) => void;\n  minHeight: string | number | undefined;\n  maxHeight: string | number | undefined;\n  className?: string;\n}\n\nfunction toCss(value: string | number | undefined): string | undefined {\n  if (value === undefined) return undefined;\n  return typeof value === \"number\" ? `${value}px` : value;\n}\n\nexport function EditorPane({ setContainer, minHeight, maxHeight, className }: EditorPaneProps) {\n  const style: CSSProperties = {\n    minHeight: toCss(minHeight) ?? \"12rem\",\n    maxHeight: toCss(maxHeight),\n  };\n\n  return (\n    <div\n      ref={setContainer}\n      style={style}\n      className={cn(\n        \"markdown-editor__editor flex-1 overflow-hidden bg-card\",\n        className,\n      )}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/markdown-editor/parts/editor-pane.tsx"
    },
    {
      "path": "src/registry/components/forms/markdown-editor/parts/kind-badge.tsx",
      "content": "import type { CSSProperties } from \"react\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { cn } from \"@/lib/utils\";\nimport type { KindMeta } from \"../types\";\n\ninterface KindBadgeProps {\n  meta: KindMeta;\n  className?: string;\n}\n\nexport function KindBadge({ meta, className }: KindBadgeProps) {\n  // If meta.color is provided, use color-mix for the background tint and the literal\n  // for text + border. Otherwise fall back to the outline variant using design tokens.\n  const styled: CSSProperties = meta.color\n    ? {\n        color: meta.color,\n        backgroundColor: `color-mix(in oklch, ${meta.color} 14%, transparent)`,\n        borderColor: meta.color,\n      }\n    : {};\n\n  return (\n    <Badge\n      variant={meta.color ? \"outline\" : \"secondary\"}\n      style={styled}\n      className={cn(\"text-[0.6875rem] font-semibold\", className)}\n    >\n      {meta.label}\n    </Badge>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/markdown-editor/parts/kind-badge.tsx"
    },
    {
      "path": "src/registry/components/forms/markdown-editor/parts/preview-pane.tsx",
      "content": "import { useCallback, type CSSProperties, type KeyboardEvent, type MouseEvent } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ninterface PreviewPaneProps {\n  html: string;\n  onWikilinkClick: ((target: string) => void) | undefined;\n  minHeight: string | number | undefined;\n  maxHeight: string | number | undefined;\n  className?: string;\n}\n\nfunction toCss(value: string | number | undefined): string | undefined {\n  if (value === undefined) return undefined;\n  return typeof value === \"number\" ? `${value}px` : value;\n}\n\n// Click + keyboard delegation per §6.5 — interactive wikilinks (those with role+tabindex)\n// activate on Enter/Space; spans without role are inert.\nexport function PreviewPane({\n  html,\n  onWikilinkClick,\n  minHeight,\n  maxHeight,\n  className,\n}: PreviewPaneProps) {\n  const activate = useCallback(\n    (target: HTMLElement) => {\n      const span = target.closest(\"[data-wikilink-target]\");\n      if (!span) return;\n      const wikilinkTarget = span.getAttribute(\"data-wikilink-target\");\n      if (wikilinkTarget && onWikilinkClick) {\n        onWikilinkClick(wikilinkTarget);\n      }\n    },\n    [onWikilinkClick],\n  );\n\n  const handleClick = useCallback(\n    (event: MouseEvent<HTMLDivElement>) => {\n      const targetEl = event.target as HTMLElement;\n      if (!targetEl.closest(\"[data-wikilink-target]\")) return;\n      event.preventDefault();\n      activate(targetEl);\n    },\n    [activate],\n  );\n\n  const handleKeyDown = useCallback(\n    (event: KeyboardEvent<HTMLDivElement>) => {\n      if (event.key !== \"Enter\" && event.key !== \" \") return;\n      const targetEl = event.target as HTMLElement;\n      if (!targetEl.matches(\"[data-wikilink-target]\")) return;\n      event.preventDefault();\n      activate(targetEl);\n    },\n    [activate],\n  );\n\n  const style: CSSProperties = {\n    minHeight: toCss(minHeight) ?? \"12rem\",\n    maxHeight: toCss(maxHeight),\n  };\n\n  return (\n    <div\n      role=\"article\"\n      onClick={handleClick}\n      onKeyDown={handleKeyDown}\n      className={cn(\n        \"markdown-editor__preview flex-1 overflow-y-auto bg-card px-4 py-3 text-sm leading-7\",\n        // Tailwind v4 typography styles for the rendered markdown — light + dark.\n        \"prose-headings:scroll-mt-4 prose-headings:font-semibold\",\n        \"[&_h1]:mb-3 [&_h1]:mt-1 [&_h1]:text-2xl\",\n        \"[&_h2]:mb-2 [&_h2]:mt-4 [&_h2]:text-xl\",\n        \"[&_h3]:mb-2 [&_h3]:mt-3 [&_h3]:text-lg\",\n        \"[&_p]:my-2\",\n        \"[&_ul]:my-2 [&_ul]:list-disc [&_ul]:pl-6\",\n        \"[&_ol]:my-2 [&_ol]:list-decimal [&_ol]:pl-6\",\n        \"[&_li]:my-0.5\",\n        \"[&_blockquote]:my-2 [&_blockquote]:border-l-2 [&_blockquote]:border-border [&_blockquote]:pl-3 [&_blockquote]:text-muted-foreground [&_blockquote]:italic\",\n        \"[&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-[0.85em]\",\n        \"[&_pre]:my-3 [&_pre]:overflow-x-auto [&_pre]:rounded-md [&_pre]:bg-muted [&_pre]:p-3 [&_pre]:font-mono [&_pre]:text-[0.85em]\",\n        \"[&_pre_code]:bg-transparent [&_pre_code]:p-0\",\n        \"[&_a]:text-primary [&_a]:underline [&_a]:underline-offset-2 hover:[&_a]:opacity-80\",\n        \"[&_strong]:font-semibold\",\n        \"[&_em]:italic\",\n        \"[&_hr]:my-4 [&_hr]:border-border\",\n        \"[&_table]:my-3 [&_table]:w-full [&_table]:border-collapse [&_table]:text-xs\",\n        \"[&_th]:border [&_th]:border-border [&_th]:bg-muted [&_th]:px-2 [&_th]:py-1 [&_th]:text-left [&_th]:font-medium\",\n        \"[&_td]:border [&_td]:border-border [&_td]:px-2 [&_td]:py-1\",\n        // Wikilink visual treatment — resolved + broken\n        \"[&_.wikilink]:rounded [&_.wikilink]:bg-primary/10 [&_.wikilink]:px-1 [&_.wikilink]:py-0.5 [&_.wikilink]:text-primary [&_.wikilink]:no-underline\",\n        \"[&_.wikilink[role=link]]:cursor-pointer [&_.wikilink[role=link]:hover]:bg-primary/15 [&_.wikilink[role=link]:focus-visible]:outline [&_.wikilink[role=link]:focus-visible]:outline-2 [&_.wikilink[role=link]:focus-visible]:outline-offset-2 [&_.wikilink[role=link]:focus-visible]:outline-ring\",\n        \"[&_.wikilink-broken]:bg-destructive/10 [&_.wikilink-broken]:text-destructive [&_.wikilink-broken]:underline [&_.wikilink-broken]:decoration-dashed [&_.wikilink-broken]:underline-offset-2\",\n        className,\n      )}\n      style={style}\n      // Safe sink (v0.1.2): `html` comes from lib/parse-markdown.ts, whose\n      // per-instance Marked ESCAPES all raw HTML tokens (block + inline) and\n      // scheme-filters link hrefs (javascript:/data:/vbscript: stripped) —\n      // untrusted multi-author markdown cannot inject markup here. Wikilink\n      // spans are generated by our renderer with escaped label/target.\n      dangerouslySetInnerHTML={{ __html: html }}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/markdown-editor/parts/preview-pane.tsx"
    },
    {
      "path": "src/registry/components/forms/markdown-editor/parts/toolbar-button.tsx",
      "content": "import { buttonVariants } from \"@/components/ui/button\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\nimport type { ToolbarCtx, ToolbarItem } from \"../types\";\n\ninterface ToolbarButtonProps {\n  item: ToolbarItem;\n  ctx: ToolbarCtx;\n  disabled?: boolean;\n}\n\nexport function ToolbarButton({ item, ctx, disabled }: ToolbarButtonProps) {\n  const isActive = item.isActive ? item.isActive(ctx) : false;\n  const tooltipLabel = item.shortcut ? `${item.label} (${item.shortcut})` : item.label;\n\n  // F-cross-13: no `asChild` — the trigger IS the toolbar button (buttonVariants styling).\n  return (\n    <Tooltip>\n      <TooltipTrigger\n        type=\"button\"\n        aria-label={item.label}\n        aria-pressed={item.isActive ? isActive : undefined}\n        aria-keyshortcuts={item.shortcut}\n        disabled={disabled}\n        onClick={() => item.run?.(ctx)}\n        className={cn(\n          buttonVariants({ variant: \"ghost\", size: \"icon-sm\" }),\n          \"text-muted-foreground hover:text-foreground\",\n          isActive && \"bg-muted text-foreground\",\n        )}\n      >\n        {item.icon}\n      </TooltipTrigger>\n      <TooltipContent>{tooltipLabel}</TooltipContent>\n    </Tooltip>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/markdown-editor/parts/toolbar-button.tsx"
    },
    {
      "path": "src/registry/components/forms/markdown-editor/parts/toolbar.tsx",
      "content": "import { ToolbarButton } from \"./toolbar-button\";\nimport { cn } from \"@/lib/utils\";\nimport type { ToolbarCtx, ToolbarItem } from \"../types\";\n\ninterface ToolbarProps {\n  items: ReadonlyArray<ToolbarItem>;\n  ctx: ToolbarCtx;\n  disabled?: boolean;\n  className?: string;\n}\n\nfunction isSeparator(item: ToolbarItem): boolean {\n  return item.id.startsWith(\"sep\");\n}\n\nexport function Toolbar({ items, ctx, disabled, className }: ToolbarProps) {\n  return (\n    <div\n      role=\"toolbar\"\n      aria-label=\"Markdown editor toolbar\"\n      className={cn(\n        \"flex flex-wrap items-center gap-1 rounded-t-md border border-b-0 border-border bg-card px-2 py-1.5\",\n        className,\n      )}\n    >\n      {items.map((item) =>\n        isSeparator(item) ? (\n          <span\n            key={item.id}\n            aria-hidden\n            className=\"mx-1 inline-block h-5 w-px bg-border\"\n          />\n        ) : (\n          <ToolbarButton key={item.id} item={item} ctx={ctx} disabled={disabled} />\n        ),\n      )}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/markdown-editor/parts/toolbar.tsx"
    },
    {
      "path": "src/registry/components/forms/markdown-editor/parts/view-toggle.tsx",
      "content": "import { Tabs, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\nimport { cn } from \"@/lib/utils\";\nimport type { ViewMode } from \"../types\";\n\ninterface ViewToggleProps {\n  value: ViewMode;\n  onChange: (next: ViewMode) => void;\n  className?: string;\n}\n\nexport function ViewToggle({ value, onChange, className }: ViewToggleProps) {\n  return (\n    <Tabs\n      value={value}\n      onValueChange={(v) => onChange(v as ViewMode)}\n      className={cn(\"w-fit shrink-0\", className)}\n    >\n      <TabsList variant=\"line\" className=\"h-7 gap-0.5\">\n        <TabsTrigger value=\"edit\" className=\"px-2 text-xs\">\n          Edit\n        </TabsTrigger>\n        <TabsTrigger value=\"split\" className=\"px-2 text-xs\">\n          Split\n        </TabsTrigger>\n        <TabsTrigger value=\"preview\" className=\"px-2 text-xs\">\n          Preview\n        </TabsTrigger>\n      </TabsList>\n    </Tabs>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/markdown-editor/parts/view-toggle.tsx"
    }
  ],
  "categories": [
    "forms",
    "graph-system"
  ],
  "type": "registry:block"
}