{"$schema":"https:\/\/ui.particle.academy\/schema\/registry-item.json","name":"editor","type":"registry:ui","title":"Editor","description":"Editor from react-fancy","package":"react-fancy","dependencies":["marked"],"registryDependencies":["content-renderer","inputs","code-view"],"files":[{"path":"components\/fancy\/editor\/Editor.context.ts","content":"import { createContext, useContext } from \"react\";\r\nimport type { RefObject } from \"react\";\r\nimport type { RenderExtension } from \"..\/ContentRenderer\/extensions\";\r\n\r\nexport interface EditorContextValue {\r\n  contentRef: RefObject<HTMLDivElement | null>;\r\n  exec: (command: string, arg?: string) => void;\r\n  insertText: (text: string) => void;\r\n  wrapSelection: (before: string, after: string) => void;\r\n  outputFormat: \"html\" | \"markdown\";\r\n  lineSpacing: number;\r\n  placeholder?: string;\r\n  \/** Merged render extensions (global + instance). *\/\r\n  extensions: RenderExtension[];\r\n  \/** Whether the raw-source view (textarea) is showing instead of the rich-text surface. *\/\r\n  showSource: boolean;\r\n  \/** Toggle\/set the raw-source view. *\/\r\n  setShowSource: (showSource: boolean) => void;\r\n  \/** Initial HTML content to load into the editor on mount *\/\r\n  _initialHtml: string;\r\n  \/** @internal Called by EditorContent on input events *\/\r\n  _onInput: () => void;\r\n  \/** @internal Raw source value (`value` in `outputFormat`), for the source textarea. *\/\r\n  _sourceValue: string;\r\n  \/** @internal Commit an edit made directly in the source textarea. *\/\r\n  _onSourceInput: (value: string) => void;\r\n}\r\n\r\nexport const EditorContext = createContext<EditorContextValue | null>(null);\r\n\r\nexport function useEditor(): EditorContextValue {\r\n  const ctx = useContext(EditorContext);\r\n  if (!ctx) {\r\n    throw new Error(\"useEditor must be used within an <Editor> component\");\r\n  }\r\n  return ctx;\r\n}\r\n","type":"registry:ui","target":"components\/fancy\/editor\/Editor.context.ts"},{"path":"components\/fancy\/editor\/Editor.tsx","content":"import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\r\nimport { marked } from \"marked\";\r\nimport { cn } from \"..\/..\/utils\/cn\";\r\nimport { sanitizeHtml } from \"..\/..\/utils\/sanitize\";\r\nimport { useControllableState } from \"..\/..\/hooks\";\r\nimport { EditorContext } from \".\/Editor.context\";\r\nimport { EditorToolbar } from \".\/EditorToolbar\";\r\nimport { EditorToolbarSeparator } from \".\/EditorToolbarSeparator\";\r\nimport { EditorSourceToggle } from \".\/EditorSourceToggle\";\r\nimport { EditorContent } from \".\/EditorContent\";\r\nimport { htmlToMarkdown, detectFormat } from \".\/editor.utils\";\r\nimport { mergeExtensions } from \"..\/ContentRenderer\/extensions\";\r\nimport { ContentRenderer } from \"..\/ContentRenderer\/ContentRenderer\";\r\nimport { useFieldMode } from \"..\/inputs\/mode\/FieldMode.context\";\r\nimport type { EditorProps } from \".\/Editor.types\";\r\n\r\nfunction toHtml(\r\n  value: string,\r\n  outputFormat: \"html\" | \"markdown\",\r\n  unsafe: boolean,\r\n  valueFormat: \"markdown\" | \"html\" | \"auto\" = \"auto\",\r\n): string {\r\n  if (!value) return \"\";\r\n  const raw = (() => {\r\n    \/\/ An explicit valueFormat is authoritative \u2014 the sniff misclassifies\r\n    \/\/ markdown that merely MENTIONS HTML-ish snippets (`<code>`, `<table`),\r\n    \/\/ collapsing it into an innerHTML wall of text (#10).\r\n    const format =\r\n      valueFormat !== \"auto\"\r\n        ? valueFormat\r\n        : outputFormat === \"html\"\r\n          ? \"html\"\r\n          : detectFormat(value);\r\n    if (format === \"html\") return value;\r\n    return (marked.parse(value, { async: false }) as string).trim();\r\n  })();\r\n  return unsafe ? raw : sanitizeHtml(raw);\r\n}\r\n\r\nfunction EditorRoot({\r\n  children,\r\n  className,\r\n  value: controlledValue,\r\n  defaultValue = \"\",\r\n  onChange,\r\n  outputFormat = \"html\",\r\n  valueFormat = \"auto\",\r\n  lineSpacing = 1.6,\r\n  placeholder,\r\n  mode: modeProp,\r\n  extensions: instanceExtensions,\r\n  unsafe = false,\r\n  showSource: showSourceProp,\r\n  defaultShowSource = false,\r\n  onShowSourceChange,\r\n}: EditorProps) {\r\n  const contentRef = useRef<HTMLDivElement | null>(null);\r\n  const [value, setValue] = useControllableState(controlledValue, defaultValue, onChange);\r\n  const [showSource, setShowSource] = useControllableState(\r\n    showSourceProp,\r\n    defaultShowSource,\r\n    onShowSourceChange,\r\n  );\r\n  \/\/ View\/edit mode \u2014 same resolution as the inputs: prop \u2192 <Form> context \u2192 \"edit\".\r\n  const mode = useFieldMode(modeProp);\r\n  const isView = mode === \"view\";\r\n\r\n  \/\/ The value this editor last emitted through `onChange`. An incoming `value`\r\n  \/\/ equal to it is our own keystroke echoing back; anything else came from\r\n  \/\/ OUTSIDE \u2014 an async load, a host reset, a form repopulating.\r\n  const lastEmitted = useRef<string | null>(null);\r\n\r\n  \/\/ Bumped only on an external `value` change, so the seed memo below recomputes\r\n  \/\/ for those and stays put while the user types. Without this, `value` was\r\n  \/\/ excluded from the deps to avoid re-seeding on keystrokes, which also meant a\r\n  \/\/ value arriving after mount was silently dropped and the editor stayed blank\r\n  \/\/ forever (#15).\r\n  const [externalSeed, setExternalSeed] = useState(0);\r\n  useEffect(() => {\r\n    if (value !== lastEmitted.current) setExternalSeed((n) => n + 1);\r\n  }, [value]);\r\n\r\n  const initialHtml = useMemo(\r\n    () => toHtml(value, outputFormat, unsafe, valueFormat),\r\n    \/\/ Seed the contentEditable from the LIVE value when (re)entering edit mode \u2014\r\n    \/\/ and when returning from the raw-source view, which may have rewritten the\r\n    \/\/ value out from under the rich-text surface. `value` itself stays out of\r\n    \/\/ the deps so typing never re-seeds (which would fight the caret);\r\n    \/\/ `externalSeed` covers the externally-driven case instead.\r\n    \/\/ eslint-disable-next-line react-hooks\/exhaustive-deps\r\n    [isView, outputFormat, unsafe, valueFormat, showSource, externalSeed],\r\n  );\r\n\r\n  const extensions = useMemo(\r\n    () => mergeExtensions(instanceExtensions),\r\n    [instanceExtensions],\r\n  );\r\n\r\n  const getOutputValue = useCallback(\r\n    (html: string): string => {\r\n      return outputFormat === \"markdown\" ? htmlToMarkdown(html) : html;\r\n    },\r\n    [outputFormat],\r\n  );\r\n\r\n  const handleInput = useCallback(() => {\r\n    const el = contentRef.current;\r\n    if (!el) return;\r\n    const next = getOutputValue(el.innerHTML);\r\n    \/\/ Record what we emitted BEFORE publishing it, so the value coming back in\r\n    \/\/ is recognised as our own and does not trigger a re-seed mid-keystroke.\r\n    lastEmitted.current = next;\r\n    setValue(next);\r\n  }, [setValue, getOutputValue]);\r\n\r\n  const exec = useCallback(\r\n    (command: string, arg?: string) => {\r\n      const el = contentRef.current;\r\n      if (!el) return;\r\n      el.focus();\r\n      document.execCommand(command, false, arg);\r\n      handleInput();\r\n    },\r\n    [handleInput],\r\n  );\r\n\r\n  const insertText = useCallback(\r\n    (text: string) => {\r\n      const el = contentRef.current;\r\n      if (!el) return;\r\n      el.focus();\r\n      document.execCommand(\"insertText\", false, text);\r\n      handleInput();\r\n    },\r\n    [handleInput],\r\n  );\r\n\r\n  \/\/ The source textarea edits `value` directly \u2014 it is already the canonical\r\n  \/\/ source in `outputFormat` (html or markdown), so no conversion round-trip.\r\n  const handleSourceInput = useCallback(\r\n    (next: string) => {\r\n      \/\/ Also an internal emission \u2014 record it so typing in the source view is\r\n      \/\/ not mistaken for an external change on every keystroke.\r\n      lastEmitted.current = next;\r\n      setValue(next);\r\n    },\r\n    [setValue],\r\n  );\r\n\r\n  const wrapSelection = useCallback(\r\n    (before: string, after: string) => {\r\n      const sel = window.getSelection();\r\n      if (!sel || sel.rangeCount === 0) return;\r\n      const range = sel.getRangeAt(0);\r\n      const selectedText = range.toString();\r\n      const el = contentRef.current;\r\n      if (!el) return;\r\n      el.focus();\r\n      document.execCommand(\"insertText\", false, `${before}${selectedText}${after}`);\r\n      handleInput();\r\n    },\r\n    [handleInput],\r\n  );\r\n\r\n  const contextValue = {\r\n    contentRef,\r\n    exec,\r\n    insertText,\r\n    wrapSelection,\r\n    outputFormat,\r\n    lineSpacing,\r\n    placeholder,\r\n    extensions,\r\n    showSource,\r\n    setShowSource,\r\n    _initialHtml: initialHtml,\r\n    _onInput: handleInput,\r\n    _sourceValue: value,\r\n    _onSourceInput: handleSourceInput,\r\n  };\r\n\r\n  \/\/ View mode \u2014 render the value read-only through ContentRenderer, matching the\r\n  \/\/ editor's own format (markdown\/html), instead of the editable toolbar+content\r\n  \/\/ tree. Driven by `mode` \/ a `<Form mode=\"view\">`, exactly like the inputs.\r\n  if (isView) {\r\n    return (\r\n      <div\r\n        data-react-fancy-editor=\"\"\r\n        data-mode=\"view\"\r\n        className={cn(\r\n          \"overflow-hidden rounded-xl border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900\",\r\n          className,\r\n        )}\r\n      >\r\n        <ContentRenderer\r\n          value={value}\r\n          format={valueFormat !== \"auto\" ? valueFormat : outputFormat}\r\n          lineSpacing={lineSpacing}\r\n          extensions={instanceExtensions}\r\n          unsafe={unsafe}\r\n          className=\"px-4 py-3\"\r\n        \/>\r\n      <\/div>\r\n    );\r\n  }\r\n\r\n  return (\r\n    <EditorContext.Provider value={contextValue}>\r\n      <div\r\n        data-react-fancy-editor=\"\"\r\n        data-mode=\"edit\"\r\n        className={cn(\r\n          \"flex flex-col overflow-hidden rounded-xl border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900\",\r\n          className,\r\n        )}\r\n      >\r\n        {children}\r\n      <\/div>\r\n    <\/EditorContext.Provider>\r\n  );\r\n}\r\n\r\nconst ToolbarWithSeparator = Object.assign(EditorToolbar, {\r\n  Separator: EditorToolbarSeparator,\r\n});\r\n\r\nexport const Editor = Object.assign(EditorRoot, {\r\n  Toolbar: ToolbarWithSeparator,\r\n  Content: EditorContent,\r\n  SourceToggle: EditorSourceToggle,\r\n});\r\n","type":"registry:ui","target":"components\/fancy\/editor\/Editor.tsx"},{"path":"components\/fancy\/editor\/Editor.types.ts","content":"import type { ReactNode } from \"react\";\r\nimport type { RenderExtension } from \"..\/ContentRenderer\/extensions\";\r\nimport type { FieldMode } from \"..\/inputs\/inputs.types\";\r\n\r\nexport interface EditorAction {\r\n  icon: ReactNode;\r\n  label: string;\r\n  command: string;\r\n  commandArg?: string;\r\n  active?: boolean;\r\n}\r\n\r\nexport interface EditorToolbarProps {\r\n  actions?: EditorAction[];\r\n  onAction?: (command: string) => void;\r\n  children?: ReactNode;\r\n  className?: string;\r\n  \/**\r\n   * Append a \"Source\" toggle (reveals the raw HTML\/Markdown behind the editor)\r\n   * to the right of the default toolbar. Ignored when `children` are supplied \u2014\r\n   * custom toolbars compose their own `<Editor.SourceToggle \/>`.\r\n   * @default true\r\n   *\/\r\n  showSourceToggle?: boolean;\r\n}\r\n\r\nexport interface EditorContentProps {\r\n  className?: string;\r\n  \/** Max height in px before scrolling. When set, content area becomes scrollable. *\/\r\n  maxHeight?: number;\r\n  \/** Class names applied to the raw-source `<textarea>` shown while source mode is on. *\/\r\n  sourceClassName?: string;\r\n}\r\n\r\nexport interface EditorSourceToggleProps {\r\n  className?: string;\r\n  \/** Toggle glyph\/label. @default `<\/>` *\/\r\n  icon?: ReactNode;\r\n  \/** Accessible title when in rich-text mode (click \u2192 source). @default \"Source\" *\/\r\n  title?: string;\r\n  \/** Accessible title when in source mode (click \u2192 rich text). @default \"Rich text\" *\/\r\n  activeTitle?: string;\r\n}\r\n\r\nexport interface EditorProps {\r\n  children: ReactNode;\r\n  className?: string;\r\n  value?: string;\r\n  defaultValue?: string;\r\n  onChange?: (value: string) => void;\r\n  outputFormat?: \"html\" | \"markdown\";\r\n  \/**\r\n   * The format of the INCOMING `value` \/ `defaultValue`. Default `\"auto\"`\r\n   * keeps the historical sniff (`detectFormat`) \u2014 but real-world markdown that\r\n   * merely mentions HTML-ish snippets (`<code>`, `<table`, \u2026) flips the sniff\r\n   * to html and renders as a wall of text. Callers with a KNOWN format\r\n   * (file-typed content, values previously emitted with\r\n   * `outputFormat=\"markdown\"`) should declare it; explicit values bypass the\r\n   * sniff entirely \u2014 in edit mode AND view mode.\r\n   * @default \"auto\"\r\n   *\/\r\n  valueFormat?: \"markdown\" | \"html\" | \"auto\";\r\n  lineSpacing?: number;\r\n  placeholder?: string;\r\n  \/**\r\n   * View\/edit mode \u2014 like the react-fancy inputs. Resolution: this prop \u2192\r\n   * nearest `<Form>` \/ `<FormProvider>` context \u2192 `\"edit\"`. In `\"view\"` mode the\r\n   * editor renders its value read-only through `<ContentRenderer>` (matching\r\n   * `outputFormat` \u2014 markdown\/html) instead of the editable toolbar + content.\r\n   * @default \"edit\"\r\n   *\/\r\n  mode?: FieldMode;\r\n  \/** Per-instance render extensions. Merged with any globally-registered extensions. *\/\r\n  extensions?: RenderExtension[];\r\n  \/**\r\n   * Controlled source-view flag. When `true` the content area shows the raw\r\n   * HTML\/Markdown (`value`, in `outputFormat`) in an editable `<textarea>`\r\n   * instead of the rich-text surface. Pair with `onShowSourceChange`, or leave\r\n   * uncontrolled and drive it with the toolbar's Source toggle.\r\n   *\/\r\n  showSource?: boolean;\r\n  \/** Initial source-view flag when uncontrolled. @default false *\/\r\n  defaultShowSource?: boolean;\r\n  \/** Fired when the Source toggle (or an agent) flips source view on\/off. *\/\r\n  onShowSourceChange?: (showSource: boolean) => void;\r\n  \/**\r\n   * Skip HTML sanitization of the initial value. By default the initial markdown\/HTML\r\n   * is sanitized to remove `<script>`, `<iframe>`, event handlers, and `javascript:`\r\n   * URIs before being placed into contentEditable. Pass `unsafe` only when the\r\n   * initial value is fully trusted.\r\n   * @default false\r\n   *\/\r\n  unsafe?: boolean;\r\n}\r\n","type":"registry:ui","target":"components\/fancy\/editor\/Editor.types.ts"},{"path":"components\/fancy\/editor\/EditorContent.tsx","content":"import { useEffect, useMemo, useRef } from \"react\";\nimport { cn } from \"..\/..\/utils\/cn\";\nimport { CodeView } from \"..\/CodeView\";\nimport { useEditor } from \".\/Editor.context\";\nimport { proseClasses, extensionEditorClasses } from \".\/editor.utils\";\nimport type { EditorContentProps } from \".\/Editor.types\";\n\nexport function EditorContent({ className, maxHeight, sourceClassName }: EditorContentProps) {\n  const {\n    contentRef,\n    lineSpacing,\n    placeholder,\n    extensions,\n    outputFormat,\n    showSource,\n    _initialHtml,\n    _onInput,\n    _sourceValue,\n    _onSourceInput,\n  } = useEditor();\n  \/\/ Tracks which seed HTML is currently in the contentEditable. Seeding on the\n  \/\/ seed VALUE (not a one-shot boolean) re-seeds when the source view rewrites\n  \/\/ `value`, while still never re-seeding on plain keystrokes \u2014 typing leaves\n  \/\/ `_initialHtml` untouched, so `seededHtml.current === _initialHtml` holds.\n  const seededHtml = useRef<string | null>(null);\n\n  useEffect(() => {\n    \/\/ Source view replaces the contentEditable in the tree, so it unmounts.\n    \/\/ Forget what we seeded \u2014 the div that returns is a fresh, empty one and\n    \/\/ must always be re-seeded, even if `value` was left untouched.\n    if (showSource) {\n      seededHtml.current = null;\n      return;\n    }\n    const el = contentRef.current;\n    if (el && seededHtml.current !== _initialHtml) {\n      el.innerHTML = _initialHtml;\n      seededHtml.current = _initialHtml;\n    }\n  }, [contentRef, _initialHtml, showSource]);\n\n  const extClasses = useMemo(\n    () => extensionEditorClasses(extensions),\n    [extensions],\n  );\n\n  if (showSource) {\n    \/\/ Raw-source view \u2014 a syntax-highlighted code surface (HTML highlighted;\n    \/\/ markdown\/plain un-highlighted), built on the shared fancy-file-commons\n    \/\/ primitives. It's a DIRECT child of the flex-column editor root (no\n    \/\/ wrapper \u2014 a nested flex box collapses the fill), so `flex-auto` makes it\n    \/\/ fill the editor body when height-constrained and grow with content\n    \/\/ (\u2265 minHeight) when not. `maxHeight` caps + scrolls.\n    return (\n      <CodeView\n        value={_sourceValue}\n        onChange={_onSourceInput}\n        language={outputFormat === \"html\" ? \"html\" : \"plaintext\"}\n        placeholder={placeholder}\n        maxHeight={maxHeight}\n        className={cn(\"min-h-0 flex-auto\", sourceClassName)}\n      \/>\n    );\n  }\n\n  return (\n    <div\n      ref={contentRef}\n      contentEditable\n      suppressContentEditableWarning\n      data-react-fancy-editor-content=\"\"\n      data-placeholder={placeholder}\n      onInput={_onInput}\n      style={{\n        lineHeight: lineSpacing,\n        maxHeight: maxHeight ? `${maxHeight}px` : undefined,\n      }}\n      className={cn(\n        \"min-h-[120px] flex-auto overflow-y-auto px-4 py-3 text-sm outline-none\",\n        \"focus:outline-none\",\n        proseClasses,\n        extClasses,\n        \"empty:before:content-[attr(data-placeholder)] empty:before:text-zinc-400 empty:before:pointer-events-none\",\n        className,\n      )}\n    \/>\n  );\n}\n\nEditorContent.displayName = \"EditorContent\";\n","type":"registry:ui","target":"components\/fancy\/editor\/EditorContent.tsx"},{"path":"components\/fancy\/editor\/EditorSourceToggle.tsx","content":"import { cn } from \"..\/..\/utils\/cn\";\nimport { useEditor } from \".\/Editor.context\";\nimport type { EditorSourceToggleProps } from \".\/Editor.types\";\n\n\/**\n * Toolbar button that reveals the raw HTML\/Markdown behind the editor. Reads\n * `showSource` \/ `setShowSource` from context, so it works in the default\n * toolbar and in any custom toolbar composed with `useEditor()`.\n *\/\nexport function EditorSourceToggle({\n  className,\n  icon = \"<\/>\",\n  title = \"Source\",\n  activeTitle = \"Rich text\",\n}: EditorSourceToggleProps) {\n  const { showSource, setShowSource } = useEditor();\n\n  return (\n    <button\n      type=\"button\"\n      onClick={() => setShowSource(!showSource)}\n      title={showSource ? activeTitle : title}\n      aria-pressed={showSource}\n      data-react-fancy-editor-source-toggle=\"\"\n      className={cn(\n        \"inline-flex h-8 items-center justify-center rounded-md px-2 font-mono text-xs transition-colors\",\n        showSource\n          ? \"bg-zinc-200 dark:bg-zinc-700\"\n          : \"hover:bg-zinc-100 dark:hover:bg-zinc-800\",\n        className,\n      )}\n    >\n      {icon}\n    <\/button>\n  );\n}\n\nEditorSourceToggle.displayName = \"EditorSourceToggle\";\n","type":"registry:ui","target":"components\/fancy\/editor\/EditorSourceToggle.tsx"},{"path":"components\/fancy\/editor\/EditorToolbar.tsx","content":"import { cn } from \"..\/..\/utils\/cn\";\r\nimport { useEditor } from \".\/Editor.context\";\r\nimport { EditorSourceToggle } from \".\/EditorSourceToggle\";\r\nimport type { EditorToolbarProps } from \".\/Editor.types\";\r\n\r\nconst DEFAULT_ACTIONS = [\r\n  { icon: \"B\", label: \"Bold\", command: \"bold\" },\r\n  { icon: \"I\", label: \"Italic\", command: \"italic\" },\r\n  { icon: \"U\", label: \"Underline\", command: \"underline\" },\r\n  { icon: \"S\", label: \"Strikethrough\", command: \"strikeThrough\" },\r\n];\r\n\r\nexport function EditorToolbar({\r\n  actions = DEFAULT_ACTIONS,\r\n  onAction,\r\n  children,\r\n  className,\r\n  showSourceToggle = true,\r\n}: EditorToolbarProps) {\r\n  const { exec, showSource } = useEditor();\r\n\r\n  return (\r\n    <div\r\n      data-react-fancy-editor-toolbar=\"\"\r\n      className={cn(\r\n        \"flex shrink-0 items-center gap-0.5 border-b border-zinc-200 px-2 py-1.5 dark:border-zinc-700\",\r\n        className,\r\n      )}\r\n    >\r\n      {children ?? (\r\n        <>\r\n          {actions.map((action) => (\r\n            <button\r\n              key={`${action.command}-${action.commandArg ?? \"\"}`}\r\n              type=\"button\"\r\n              \/\/ Rich-text commands don't apply to the raw-source textarea.\r\n              disabled={showSource}\r\n              onClick={() => {\r\n                if (onAction) {\r\n                  onAction(action.command);\r\n                } else {\r\n                  exec(action.command, action.commandArg);\r\n                }\r\n              }}\r\n              title={action.label}\r\n              className={cn(\r\n                \"inline-flex h-8 w-8 items-center justify-center rounded-md text-sm transition-colors\",\r\n                action.active\r\n                  ? \"bg-zinc-200 dark:bg-zinc-700\"\r\n                  : \"hover:bg-zinc-100 dark:hover:bg-zinc-800\",\r\n                showSource && \"cursor-not-allowed opacity-40 hover:bg-transparent\",\r\n              )}\r\n            >\r\n              {action.icon}\r\n            <\/button>\r\n          ))}\r\n          {showSourceToggle && <EditorSourceToggle className=\"ml-auto\" \/>}\r\n        <\/>\r\n      )}\r\n    <\/div>\r\n  );\r\n}\r\n\r\nEditorToolbar.displayName = \"EditorToolbar\";\r\n","type":"registry:ui","target":"components\/fancy\/editor\/EditorToolbar.tsx"},{"path":"components\/fancy\/editor\/EditorToolbarSeparator.tsx","content":"export function EditorToolbarSeparator() {\r\n  return (\r\n    <span\r\n      data-react-fancy-editor-toolbar-separator=\"\"\r\n      className=\"mx-1 h-5 w-px bg-zinc-200 dark:bg-zinc-700\"\r\n    \/>\r\n  );\r\n}\r\n\r\nEditorToolbarSeparator.displayName = \"EditorToolbarSeparator\";\r\n","type":"registry:ui","target":"components\/fancy\/editor\/EditorToolbarSeparator.tsx"},{"path":"components\/fancy\/editor\/editor.utils.ts","content":"import type { RenderExtension } from \"..\/ContentRenderer\/extensions\";\r\n\r\n\/**\r\n * Shared prose typography classes for Editor.Content and ContentRenderer.\r\n * Hand-rolled since we don't depend on @tailwindcss\/typography.\r\n *\/\r\nexport const proseClasses = [\r\n  \"[&_h1]:text-2xl [&_h1]:font-bold [&_h1]:mt-4 [&_h1]:mb-2\",\r\n  \"[&_h2]:text-xl [&_h2]:font-semibold [&_h2]:mt-3 [&_h2]:mb-2\",\r\n  \"[&_h3]:text-lg [&_h3]:font-semibold [&_h3]:mt-2 [&_h3]:mb-1\",\r\n  \"[&_p]:mb-2\",\r\n  \"[&_a]:text-blue-500 [&_a]:underline\",\r\n  \"[&_ul]:list-disc [&_ul]:pl-5 [&_ol]:list-decimal [&_ol]:pl-5\",\r\n  \"[&_li]:mb-1\",\r\n  \"[&_blockquote]:border-l-4 [&_blockquote]:border-zinc-300 [&_blockquote]:pl-4 [&_blockquote]:italic [&_blockquote]:text-zinc-600 dark:[&_blockquote]:border-zinc-600 dark:[&_blockquote]:text-zinc-400\",\r\n  \"[&_code]:bg-zinc-100 [&_code]:px-1 [&_code]:rounded [&_code]:text-sm dark:[&_code]:bg-zinc-800\",\r\n  \"[&_pre]:bg-zinc-950 [&_pre]:text-zinc-100 [&_pre]:p-4 [&_pre]:rounded-lg [&_pre]:overflow-x-auto\",\r\n  \"[&_pre_code]:bg-transparent [&_pre_code]:px-0\",\r\n  \"[&_strong]:font-bold [&_em]:italic\",\r\n].join(\" \");\r\n\r\nconst HTML_TAG_PATTERN = \/<(p|div|br|h[1-6]|ul|ol|strong|em|a |img |table|pre|code|blockquote)[>\\s\/]\/i;\r\n\r\n\/** Detect whether a string is likely HTML or Markdown. *\/\r\nexport function detectFormat(value: string): \"html\" | \"markdown\" {\r\n  return HTML_TAG_PATTERN.test(value) ? \"html\" : \"markdown\";\r\n}\r\n\r\n\/**\r\n * Build a set of known HTML tag names. Extension tags are excluded\r\n * so that the \"strip remaining tags\" step preserves them.\r\n *\/\r\nconst STANDARD_HTML_TAGS = new Set([\r\n  \"a\", \"abbr\", \"address\", \"area\", \"article\", \"aside\", \"audio\",\r\n  \"b\", \"base\", \"bdi\", \"bdo\", \"blockquote\", \"body\", \"br\", \"button\",\r\n  \"canvas\", \"caption\", \"cite\", \"code\", \"col\", \"colgroup\",\r\n  \"data\", \"datalist\", \"dd\", \"del\", \"details\", \"dfn\", \"dialog\", \"div\", \"dl\", \"dt\",\r\n  \"em\", \"embed\",\r\n  \"fieldset\", \"figcaption\", \"figure\", \"footer\", \"form\",\r\n  \"h1\", \"h2\", \"h3\", \"h4\", \"h5\", \"h6\", \"head\", \"header\", \"hgroup\", \"hr\", \"html\",\r\n  \"i\", \"iframe\", \"img\", \"input\", \"ins\",\r\n  \"kbd\",\r\n  \"label\", \"legend\", \"li\", \"link\",\r\n  \"main\", \"map\", \"mark\", \"menu\", \"meta\", \"meter\",\r\n  \"nav\", \"noscript\",\r\n  \"object\", \"ol\", \"optgroup\", \"option\", \"output\",\r\n  \"p\", \"param\", \"picture\", \"pre\", \"progress\",\r\n  \"q\",\r\n  \"rp\", \"rt\", \"ruby\",\r\n  \"s\", \"samp\", \"script\", \"section\", \"select\", \"slot\", \"small\", \"source\", \"span\",\r\n  \"strike\", \"strong\", \"style\", \"sub\", \"summary\", \"sup\",\r\n  \"table\", \"tbody\", \"td\", \"template\", \"textarea\", \"tfoot\", \"th\", \"thead\", \"time\", \"title\", \"tr\", \"track\",\r\n  \"u\", \"ul\",\r\n  \"var\", \"video\",\r\n  \"wbr\",\r\n]);\r\n\r\n\/**\r\n * Lightweight HTML-to-Markdown converter for editor output.\r\n * Handles the most common inline\/block formatting produced by contentEditable.\r\n * Custom extension tags are preserved as-is in the output.\r\n *\/\r\nexport function htmlToMarkdown(html: string): string {\r\n  let md = html;\r\n\r\n  \/\/ Block elements first\r\n  md = md.replace(\/<h1[^>]*>(.*?)<\\\/h1>\/gi, \"# $1\\n\");\r\n  md = md.replace(\/<h2[^>]*>(.*?)<\\\/h2>\/gi, \"## $1\\n\");\r\n  md = md.replace(\/<h3[^>]*>(.*?)<\\\/h3>\/gi, \"### $1\\n\");\r\n  md = md.replace(\/<h4[^>]*>(.*?)<\\\/h4>\/gi, \"#### $1\\n\");\r\n  md = md.replace(\/<h5[^>]*>(.*?)<\\\/h5>\/gi, \"##### $1\\n\");\r\n  md = md.replace(\/<h6[^>]*>(.*?)<\\\/h6>\/gi, \"###### $1\\n\");\r\n\r\n  \/\/ Lists\r\n  md = md.replace(\/<li[^>]*>(.*?)<\\\/li>\/gi, \"- $1\\n\");\r\n  md = md.replace(\/<\\\/?[uo]l[^>]*>\/gi, \"\\n\");\r\n\r\n  \/\/ Inline formatting\r\n  md = md.replace(\/<strong[^>]*>(.*?)<\\\/strong>\/gi, \"**$1**\");\r\n  md = md.replace(\/<b[^>]*>(.*?)<\\\/b>\/gi, \"**$1**\");\r\n  md = md.replace(\/<em[^>]*>(.*?)<\\\/em>\/gi, \"*$1*\");\r\n  md = md.replace(\/<i[^>]*>(.*?)<\\\/i>\/gi, \"*$1*\");\r\n  md = md.replace(\/<u[^>]*>(.*?)<\\\/u>\/gi, \"$1\");\r\n  md = md.replace(\/<s[^>]*>(.*?)<\\\/s>\/gi, \"~~$1~~\");\r\n  md = md.replace(\/<strike[^>]*>(.*?)<\\\/strike>\/gi, \"~~$1~~\");\r\n  md = md.replace(\/<del[^>]*>(.*?)<\\\/del>\/gi, \"~~$1~~\");\r\n  md = md.replace(\/<code[^>]*>(.*?)<\\\/code>\/gi, \"`$1`\");\r\n\r\n  \/\/ Links\r\n  md = md.replace(\/<a[^>]*href=\"([^\"]*)\"[^>]*>(.*?)<\\\/a>\/gi, \"[$2]($1)\");\r\n\r\n  \/\/ Blockquotes\r\n  md = md.replace(\/<blockquote[^>]*>(.*?)<\\\/blockquote>\/gi, \"> $1\\n\");\r\n\r\n  \/\/ Line breaks and paragraphs\r\n  md = md.replace(\/<br\\s*\\\/?>\/gi, \"\\n\");\r\n  md = md.replace(\/<p[^>]*>(.*?)<\\\/p>\/gi, \"$1\\n\\n\");\r\n  md = md.replace(\/<div[^>]*>(.*?)<\\\/div>\/gi, \"$1\\n\");\r\n\r\n  \/\/ Strip only standard HTML tags \u2014 preserve unknown\/custom tags\r\n  md = md.replace(\/<\\\/?([a-z][a-z0-9]*)\\b[^>]*>\/gi, (match, tagName: string) => {\r\n    return STANDARD_HTML_TAGS.has(tagName.toLowerCase()) ? \"\" : match;\r\n  });\r\n\r\n  \/\/ Decode common HTML entities. Decode &amp; LAST so we never double-unescape:\r\n  \/\/ e.g. \"&amp;lt;\" must decode to the literal \"&lt;\", not to \"<\".\r\n  md = md.replace(\/&lt;\/g, \"<\");\r\n  md = md.replace(\/&gt;\/g, \">\");\r\n  md = md.replace(\/&quot;\/g, '\"');\r\n  md = md.replace(\/&#39;\/g, \"'\");\r\n  md = md.replace(\/&nbsp;\/g, \" \");\r\n  md = md.replace(\/&amp;\/g, \"&\");\r\n\r\n  \/\/ Clean up extra whitespace\r\n  md = md.replace(\/\\n{3,}\/g, \"\\n\\n\");\r\n  md = md.trim();\r\n\r\n  return md;\r\n}\r\n\r\n\/**\r\n * Generate CSS classes to visually distinguish custom extension tags\r\n * inside the contentEditable area. Returns Tailwind-style descendant selectors.\r\n *\/\r\nexport function extensionEditorClasses(extensions: RenderExtension[]): string {\r\n  if (extensions.length === 0) return \"\";\r\n\r\n  return extensions\r\n    .map((ext) => {\r\n      const tag = ext.tag.toLowerCase();\r\n      return [\r\n        `[&_${tag}]:block`,\r\n        `[&_${tag}]:my-2`,\r\n        `[&_${tag}]:rounded-lg`,\r\n        `[&_${tag}]:border`,\r\n        `[&_${tag}]:border-dashed`,\r\n        `[&_${tag}]:border-zinc-300`,\r\n        `[&_${tag}]:bg-zinc-50`,\r\n        `[&_${tag}]:p-3`,\r\n        `[&_${tag}]:text-xs`,\r\n        `dark:[&_${tag}]:border-zinc-600`,\r\n        `dark:[&_${tag}]:bg-zinc-800\/50`,\r\n      ].join(\" \");\r\n    })\r\n    .join(\" \");\r\n}\r\n","type":"registry:ui","target":"components\/fancy\/editor\/editor.utils.ts"},{"path":"components\/fancy\/editor\/index.ts","content":"export { Editor } from \".\/Editor\";\r\nexport { useEditor } from \".\/Editor.context\";\r\nexport type { EditorContextValue } from \".\/Editor.context\";\r\nexport type {\r\n  EditorProps,\r\n  EditorToolbarProps,\r\n  EditorContentProps,\r\n  EditorSourceToggleProps,\r\n  EditorAction,\r\n} from \".\/Editor.types\";\r\n","type":"registry:ui","target":"components\/fancy\/editor\/index.ts"}]}