{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "rich-text-editor",
  "title": "Rich Text Editor",
  "type": "registry:ui",
  "description": "WYSIWYG editor wrapped around TipTap — bold/italic/links/lists/headings/blockquote/code, plus a configurable toolbar. Drop into forms where Markdown is too low-level.",
  "categories": [
    "form"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/components/rich-text-editor/rich-text-editor.ts",
      "content": "import { LitElement, css, html, isServer, nothing, unsafeCSS, type TemplateResult } from 'lit'\nimport { live } from 'lit/directives/live.js'\nimport { styleMap } from 'lit/directives/style-map.js'\nimport type { Editor } from '@tiptap/core'\nimport {\n  AlignCenter,\n  AlignLeft,\n  AlignRight,\n  Bold,\n  ChevronDown,\n  Code,\n  Heading1,\n  Heading2,\n  Italic,\n  Link as LinkIcon,\n  List,\n  ListChecks,\n  ListOrdered,\n  Minus,\n  Quote,\n  Redo2,\n  RemoveFormatting,\n  Strikethrough,\n  Underline as UnderlineIcon,\n  Undo2,\n  type IconNode,\n} from 'lucide'\nimport { cn } from '../../lib/utils'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\nimport { icon } from '../../lib/icon'\nimport '../toggle/toggle'\nimport '../separator/separator'\nimport '../button/button'\nimport '../input/input'\nimport '../popover/popover'\nimport type { UipInput } from '../input/input'\n\n// Copied verbatim from React's `richTextEditorCss` (which React injects as a\n// <style> tag): placeholder, compact prose overrides, task list, links.\nconst richTextEditorCss = `\n/* Editor placeholder */\n.rich-text-editor .tiptap p.is-editor-empty:first-child::before {\n  content: attr(data-placeholder);\n  float: left;\n  color: var(--muted-foreground);\n  opacity: 0.5;\n  pointer-events: none;\n  height: 0;\n}\n\n/* Prose overrides for compact styling */\n.rich-text-content .tiptap {\n  min-height: inherit;\n}\n\n.rich-text-content .tiptap > *:first-child {\n  margin-top: 0;\n}\n\n.rich-text-content .tiptap > *:last-child {\n  margin-bottom: 0;\n}\n\n.rich-text-content .tiptap h1 {\n  font-size: 1.25rem;\n  font-weight: 700;\n  line-height: 1.3;\n  margin-top: 1rem;\n  margin-bottom: 0.5rem;\n}\n\n.rich-text-content .tiptap h2 {\n  font-size: 1.1rem;\n  font-weight: 600;\n  line-height: 1.3;\n  margin-top: 0.75rem;\n  margin-bottom: 0.375rem;\n}\n\n.rich-text-content .tiptap p {\n  font-size: 0.875rem;\n  line-height: 1.6;\n  margin-top: 0.25rem;\n  margin-bottom: 0.25rem;\n}\n\n.rich-text-content .tiptap ul,\n.rich-text-content .tiptap ol {\n  padding-left: 1.25rem;\n  margin-top: 0.25rem;\n  margin-bottom: 0.25rem;\n}\n\n.rich-text-content .tiptap li {\n  font-size: 0.875rem;\n  margin-top: 0.125rem;\n  margin-bottom: 0.125rem;\n}\n\n.rich-text-content .tiptap blockquote {\n  border-left: 3px solid var(--border);\n  padding-left: 0.75rem;\n  margin-top: 0.5rem;\n  margin-bottom: 0.5rem;\n  color: var(--muted-foreground);\n  font-style: italic;\n}\n\n.rich-text-content .tiptap code {\n  background: var(--muted);\n  border-radius: 0.25rem;\n  padding: 0.125rem 0.25rem;\n  font-size: 0.8rem;\n  font-family: ui-monospace, monospace;\n}\n\n.rich-text-content .tiptap pre {\n  background: var(--muted);\n  border-radius: 0.5rem;\n  padding: 0.75rem 1rem;\n  margin-top: 0.5rem;\n  margin-bottom: 0.5rem;\n}\n\n.rich-text-content .tiptap pre code {\n  background: none;\n  padding: 0;\n  font-size: 0.8rem;\n}\n\n.rich-text-content .tiptap hr {\n  border-color: var(--border);\n  margin-top: 0.75rem;\n  margin-bottom: 0.75rem;\n}\n\n/* Task list styling */\n.rich-text-content .tiptap ul[data-type='taskList'] {\n  list-style: none;\n  padding-left: 0;\n}\n\n.rich-text-content .tiptap ul[data-type='taskList'] li {\n  display: flex;\n  align-items: flex-start;\n  gap: 0.5rem;\n}\n\n.rich-text-content .tiptap ul[data-type='taskList'] li > label {\n  flex-shrink: 0;\n  margin-top: 0.125rem;\n}\n\n.rich-text-content .tiptap ul[data-type='taskList'] li > label input[type='checkbox'] {\n  accent-color: var(--primary);\n  width: 0.875rem;\n  height: 0.875rem;\n  cursor: pointer;\n}\n\n.rich-text-content .tiptap ul[data-type='taskList'] li > div {\n  flex: 1;\n}\n\n/* Link styling */\n.rich-text-content .tiptap a {\n  color: var(--primary);\n  text-decoration: underline;\n  cursor: pointer;\n}\n`\n\n// TipTap's own base stylesheet (@tiptap/core src/style.ts, verbatim). TipTap\n// injects it into document.head, which can't reach this shadow root, so the\n// element turns `injectCSS` off and adopts it here instead.\nconst tiptapCoreCss = `.ProseMirror {\n  position: relative;\n}\n\n.ProseMirror {\n  word-wrap: break-word;\n  white-space: pre-wrap;\n  white-space: break-spaces;\n  -webkit-font-variant-ligatures: none;\n  font-variant-ligatures: none;\n  font-feature-settings: \"liga\" 0; /* the above doesn't seem to work in Edge */\n}\n\n.ProseMirror [contenteditable=\"false\"] {\n  white-space: normal;\n}\n\n.ProseMirror [contenteditable=\"false\"] [contenteditable=\"true\"] {\n  white-space: pre-wrap;\n}\n\n.ProseMirror pre {\n  white-space: pre-wrap;\n}\n\nimg.ProseMirror-separator {\n  display: inline !important;\n  border: none !important;\n  margin: 0 !important;\n  width: 0 !important;\n  height: 0 !important;\n}\n\n.ProseMirror-gapcursor {\n  display: none;\n  pointer-events: none;\n  position: absolute;\n  margin: 0;\n}\n\n.ProseMirror-gapcursor:after {\n  content: \"\";\n  display: block;\n  position: absolute;\n  top: -2px;\n  width: 20px;\n  border-top: 1px solid black;\n  animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite;\n}\n\n@keyframes ProseMirror-cursor-blink {\n  to {\n    visibility: hidden;\n  }\n}\n\n.ProseMirror-hideselection *::selection {\n  background: transparent;\n}\n\n.ProseMirror-hideselection *::-moz-selection {\n  background: transparent;\n}\n\n.ProseMirror-hideselection * {\n  caret-color: transparent;\n}\n\n.ProseMirror-focused .ProseMirror-gapcursor {\n  display: block;\n}`\n\ninterface ToolbarItem {\n  type: 'button' | 'separator' | 'link'\n  icon?: [IconNode, string, string?]\n  action?: () => void\n  isActive?: () => boolean\n  title?: string\n}\n\n// React's Toggle `className`, applied to uip-toggle's inner button through\n// ::part (rules from this shadow root win over the toggle's own classes, the\n// same result twMerge gives React).\nconst toggleClass =\n  '[&::part(base)]:focus-visible:ring-ring [&::part(base)]:size-7 [&::part(base)]:p-0 [&::part(base)]:focus-visible:ring-2 [&::part(base)]:focus-visible:outline-none'\n// React's `<Separator orientation=\"vertical\" className=\"mx-1 h-5\" />`.\nconst separatorClass = '[&::part(base)]:mx-1 [&::part(base)]:h-5'\n\n// Internal controls fire their own input / change / open-change / pressed-change\n// (and ProseMirror's contenteditable fires native `input`); all are composed and\n// would surface on the host. Only the editor's own events should.\nconst stop = (e: Event) => e.stopPropagation()\n\n/**\n * <uip-rich-text-editor> — the registry RichTextEditor (TipTap) as a web component.\n *\n *   <uip-rich-text-editor value=\"<p>Hello</p>\" placeholder=\"Write…\" min-height=\"150px\"></uip-rich-text-editor>\n *\n * Same extensions, toolbar (essential row + collapsible extended row), link\n * popover and content styles as React. TipTap is imported lazily in\n * `firstUpdated` (SSR-safe) and destroyed in `disconnectedCallback`\n * (re-created if the element is re-connected). Like React, the toolbar only\n * renders once the editor exists.\n *\n * Props: `value` (HTML; the attribute is the initial / reset value, the\n * property is live and setting it replaces the content without emitting),\n * `placeholder` (default \"Start writing...\"), `min-height` (default \"120px\"),\n * `name` (form field name — an addition: the element is form-associated and\n * submits its HTML).\n *\n * Styling: React's `className` → classes on the host for layout; the root\n * box is `::part(base)`, the editor surface (React's `editorClassName`) is\n * `::part(content)`, the toolbar is `::part(toolbar)`.\n *\n * Events: `input` on every edit and `value-change` (React's `onValueChange`,\n * `detail` = { value: HTML }), both after `.value` is updated; `change` when focus leaves\n * the element after an edit.\n *\n * Shadow DOM: ProseMirror resolves `view.root` to this shadow root and reads\n * the selection from `shadowRoot.getSelection()` (Chromium), falls back to\n * `document.getSelection()` (Firefox, which reports shadow nodes there) and\n * uses `getComposedRanges` on Safari — so no patching is needed here.\n * `focus()` on the host focuses the editor.\n */\nexport class UipRichTextEditor extends LitElement {\n  static formAssociated = true\n  // :host display can't be a utility class (the host has no template of its own).\n  // Plus React's injected stylesheet and TipTap's core stylesheet (see above).\n  static styles = [tailwind, css`:host { display: block; }`, unsafeCSS(richTextEditorCss), unsafeCSS(tiptapCoreCss)]\n\n  static properties = {\n    value: {},\n    placeholder: {},\n    minHeight: { attribute: 'min-height' },\n    name: { reflect: true },\n    showExtended: { state: true },\n    linkOpen: { state: true },\n    linkUrl: { state: true },\n    ready: { state: true },\n  }\n\n  value = ''\n  placeholder = 'Start writing...'\n  minHeight = '120px'\n  name?: string\n  private showExtended = false\n  private linkOpen = false\n  private linkUrl = ''\n  private ready = false\n\n  private editor?: Editor\n  private internals = this.attachInternals()\n  private defaultValue = ''\n  private valueAtFocus?: string\n  private initToken = 0\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'rich-text-editor')\n    this.defaultValue = this.getAttribute('value') ?? ''\n    this.addEventListener('focusin', this.onFocusIn)\n    this.addEventListener('focusout', this.onFocusOut)\n    // Re-connected after a move: firstUpdated won't run again.\n    if (this.hasUpdated && !this.editor) this.initEditor()\n  }\n\n  disconnectedCallback() {\n    super.disconnectedCallback()\n    this.removeEventListener('focusin', this.onFocusIn)\n    this.removeEventListener('focusout', this.onFocusOut)\n    this.initToken++\n    this.editor?.destroy()\n    this.editor = undefined\n    this.ready = false\n    this.linkOpen = false\n  }\n\n  protected firstUpdated() {\n    this.initEditor()\n  }\n\n  private async initEditor() {\n    if (isServer) return\n    const token = ++this.initToken\n    const [core, starter, placeholder, underline, link, textAlign, taskList, taskItem] = await Promise.all([\n      import('@tiptap/core'),\n      import('@tiptap/starter-kit'),\n      import('@tiptap/extension-placeholder'),\n      import('@tiptap/extension-underline'),\n      import('@tiptap/extension-link'),\n      import('@tiptap/extension-text-align'),\n      import('@tiptap/extension-task-list'),\n      import('@tiptap/extension-task-item'),\n    ])\n    // Disconnected (or re-initialised) while the chunks were loading.\n    if (token !== this.initToken || !this.isConnected) return\n    const mount = this.renderRoot.querySelector<HTMLElement>('[part=content]')\n    if (!mount) return\n    this.editor = new core.Editor({\n      element: mount,\n      content: this.value,\n      // TipTap would inject its base CSS into document.head; it's in static styles instead.\n      injectCSS: false,\n      extensions: [\n        starter.StarterKit.configure({\n          heading: { levels: [1, 2, 3] },\n          // StarterKit ships its own link + underline since v3 — disabled so the\n          // standalone packages can be registered with their own configuration.\n          link: false,\n          underline: false,\n        }),\n        placeholder.Placeholder.configure({ placeholder: this.placeholder }),\n        underline.Underline,\n        link.Link.configure({\n          openOnClick: false,\n          HTMLAttributes: { class: 'text-primary underline cursor-pointer' },\n        }),\n        textAlign.TextAlign.configure({ types: ['heading', 'paragraph'] }),\n        taskList.TaskList,\n        taskItem.TaskItem.configure({ nested: true }),\n      ],\n      editorProps: {\n        attributes: {\n          class: 'prose prose-sm dark:prose-invert max-w-none focus:outline-none',\n        },\n      },\n      onUpdate: ({ editor }) => {\n        this.value = editor.getHTML()\n        this.dispatchEvent(new Event('input', { bubbles: true, composed: true }))\n        this.dispatchEvent(new CustomEvent('value-change', { detail: { value: this.value }, bubbles: true, composed: true }))\n      },\n      // Re-render on every transaction so the toolbar tracks the selection.\n      onTransaction: () => this.requestUpdate(),\n    })\n    this.ready = true\n  }\n\n  protected willUpdate(changed: Map<string, unknown>) {\n    if (changed.has('value')) {\n      this.internals.setFormValue(this.value ?? '')\n      // External value change (not one the editor just emitted).\n      if (this.editor && this.editor.getHTML() !== this.value) {\n        this.editor.commands.setContent(this.value || '', { emitUpdate: false })\n      }\n    }\n  }\n\n  formResetCallback() {\n    this.value = this.defaultValue\n  }\n\n  override focus(options?: FocusOptions) {\n    if (this.editor) this.editor.commands.focus()\n    else super.focus(options)\n  }\n\n  // `change` like a native field: once focus leaves the whole element after an\n  // edit. Focus moving to the toolbar or the link popover (same shadow tree,\n  // so relatedTarget is retargeted to the host) doesn't count.\n  private onFocusIn = () => {\n    this.valueAtFocus ??= this.value\n  }\n\n  private onFocusOut = (e: FocusEvent) => {\n    if (e.relatedTarget === this) return\n    const before = this.valueAtFocus\n    this.valueAtFocus = undefined\n    if (before !== undefined && before !== this.value) {\n      this.dispatchEvent(new Event('change', { bubbles: true, composed: true }))\n    }\n  }\n\n  // --- link popover (React replaced window.prompt with this) -----------------\n  private onLinkOpenChange(e: CustomEvent<{ open: boolean }>) {\n    e.stopPropagation()\n    this.linkOpen = e.detail.open\n    // Prefill with the current href so the popover edits instead of replaces.\n    if (this.linkOpen) this.linkUrl = (this.editor?.getAttributes('link').href as string | undefined) ?? ''\n  }\n\n  private applyLink() {\n    const url = this.linkUrl.trim()\n    if (!this.editor || !url) return\n    this.editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run()\n    this.linkOpen = false\n  }\n\n  private removeLink() {\n    this.editor?.chain().focus().extendMarkRange('link').unsetLink().run()\n    this.linkUrl = ''\n    this.linkOpen = false\n  }\n\n  private get essentialItems(): ToolbarItem[] {\n    const e = this.editor\n    if (!e) return []\n    return [\n      { type: 'button', icon: [Bold, 'bold'], action: () => e.chain().focus().toggleBold().run(), isActive: () => e.isActive('bold'), title: 'Bold' },\n      { type: 'button', icon: [Italic, 'italic'], action: () => e.chain().focus().toggleItalic().run(), isActive: () => e.isActive('italic'), title: 'Italic' },\n      { type: 'button', icon: [UnderlineIcon, 'underline'], action: () => e.chain().focus().toggleUnderline().run(), isActive: () => e.isActive('underline'), title: 'Underline' },\n      { type: 'button', icon: [Strikethrough, 'strikethrough'], action: () => e.chain().focus().toggleStrike().run(), isActive: () => e.isActive('strike'), title: 'Strikethrough' },\n      { type: 'separator' },\n      { type: 'button', icon: [List, 'list'], action: () => e.chain().focus().toggleBulletList().run(), isActive: () => e.isActive('bulletList'), title: 'Bullet list' },\n      { type: 'button', icon: [ListOrdered, 'list-ordered'], action: () => e.chain().focus().toggleOrderedList().run(), isActive: () => e.isActive('orderedList'), title: 'Numbered list' },\n      { type: 'separator' },\n      { type: 'link', icon: [LinkIcon, 'link'], isActive: () => e.isActive('link'), title: 'Link' },\n      { type: 'separator' },\n      { type: 'button', icon: [Undo2, 'undo-2'], action: () => e.chain().focus().undo().run(), isActive: () => false, title: 'Undo' },\n      { type: 'button', icon: [Redo2, 'redo-2'], action: () => e.chain().focus().redo().run(), isActive: () => false, title: 'Redo' },\n    ]\n  }\n\n  private get extendedItems(): ToolbarItem[] {\n    const e = this.editor\n    if (!e) return []\n    return [\n      { type: 'button', icon: [Heading1, 'heading-1'], action: () => e.chain().focus().toggleHeading({ level: 1 }).run(), isActive: () => e.isActive('heading', { level: 1 }), title: 'Heading 1' },\n      { type: 'button', icon: [Heading2, 'heading-2'], action: () => e.chain().focus().toggleHeading({ level: 2 }).run(), isActive: () => e.isActive('heading', { level: 2 }), title: 'Heading 2' },\n      { type: 'separator' },\n      { type: 'button', icon: [Code, 'code'], action: () => e.chain().focus().toggleCode().run(), isActive: () => e.isActive('code'), title: 'Inline code' },\n      { type: 'button', icon: [Quote, 'quote'], action: () => e.chain().focus().toggleBlockquote().run(), isActive: () => e.isActive('blockquote'), title: 'Blockquote' },\n      { type: 'button', icon: [Minus, 'minus'], action: () => e.chain().focus().setHorizontalRule().run(), isActive: () => false, title: 'Divider' },\n      { type: 'button', icon: [ListChecks, 'list-checks'], action: () => e.chain().focus().toggleTaskList().run(), isActive: () => e.isActive('taskList'), title: 'Task list' },\n      { type: 'separator' },\n      { type: 'button', icon: [AlignLeft, 'text-align-start', 'lucide-text lucide-align-left'], action: () => e.chain().focus().setTextAlign('left').run(), isActive: () => e.isActive({ textAlign: 'left' }), title: 'Align left' },\n      { type: 'button', icon: [AlignCenter, 'text-align-center', 'lucide-align-center'], action: () => e.chain().focus().setTextAlign('center').run(), isActive: () => e.isActive({ textAlign: 'center' }), title: 'Align center' },\n      { type: 'button', icon: [AlignRight, 'text-align-end', 'lucide-align-right'], action: () => e.chain().focus().setTextAlign('right').run(), isActive: () => e.isActive({ textAlign: 'right' }), title: 'Align right' },\n      { type: 'separator' },\n      { type: 'button', icon: [RemoveFormatting, 'remove-formatting'], action: () => e.chain().focus().clearNodes().unsetAllMarks().run(), isActive: () => false, title: 'Clear formatting' },\n    ]\n  }\n\n  private renderToggle(item: ToolbarItem, onClick: () => void, slot?: string) {\n    // `live`: uip-toggle flips its own `pressed` on click; the editor's state\n    // (not the click) decides it, so re-assert it on every render.\n    return html`<uip-toggle\n      slot=${slot ?? nothing}\n      size=\"sm\"\n      .pressed=${live(!!item.isActive?.())}\n      title=${item.title ?? nothing}\n      aria-label=${item.title ?? nothing}\n      class=${toggleClass}\n      @click=${() => {\n        onClick()\n        this.requestUpdate()\n      }}\n      >${item.icon ? icon(item.icon[0], item.icon[1], cn(item.icon[2], 'size-3.5')) : nothing}</uip-toggle\n    >`\n  }\n\n  private renderItem(item: ToolbarItem): TemplateResult {\n    if (item.type === 'separator') {\n      return html`<uip-separator orientation=\"vertical\" class=${separatorClass}></uip-separator>`\n    }\n    if (item.type === 'link') {\n      const active = !!item.isActive?.()\n      return html`<uip-popover\n        align=\"start\"\n        class=\"[&::part(content)]:w-72 [&::part(content)]:p-3\"\n        .open=${this.linkOpen}\n        @open-change=${this.onLinkOpenChange}\n      >\n        ${this.renderToggle(item, () => {}, 'trigger')}\n        <form\n          class=\"flex flex-col gap-2\"\n          @submit=${(e: Event) => {\n            e.preventDefault()\n            this.applyLink()\n          }}\n        >\n          <label for=\"link-url\" class=\"text-foreground text-xs font-medium\">Link URL</label>\n          <uip-input\n            id=\"link-url\"\n            .value=${live(this.linkUrl)}\n            type=\"url\"\n            size=\"small\"\n            placeholder=\"https://example.com\"\n            autocomplete=\"url\"\n            @input=${(e: Event) => (this.linkUrl = (e.target as UipInput).value)}\n            @keydown=${(e: KeyboardEvent) => {\n              // The inner <input> is in uip-input's shadow root, so Enter can't\n              // implicitly submit this form.\n              if (e.key === 'Enter' && !e.isComposing) {\n                e.preventDefault()\n                this.applyLink()\n              }\n            }}\n          ></uip-input>\n          <div class=\"flex items-center justify-end gap-2\">\n            ${active\n              ? html`<uip-button type=\"button\" variant=\"ghost\" size=\"sm\" @click=${this.removeLink}>Remove</uip-button>`\n              : nothing}\n            <uip-button type=\"submit\" size=\"sm\" ?disabled=${!this.linkUrl.trim()}>Apply</uip-button>\n          </div>\n        </form>\n      </uip-popover>`\n    }\n    return this.renderToggle(item, () => item.action?.())\n  }\n\n  private renderToolbar() {\n    const expandLabel = this.showExtended ? 'Hide more options' : 'Show more options'\n    return html`<div\n      part=\"toolbar\"\n      class=\"border-b\"\n      role=\"toolbar\"\n      aria-label=\"Text formatting\"\n    >\n      <!-- Essential row -->\n      <div class=\"flex items-center gap-0.5 px-2 py-1.5\">\n        ${this.essentialItems.map((item) => this.renderItem(item))}\n        <uip-separator orientation=\"vertical\" class=${separatorClass}></uip-separator>\n        <!-- Expand toggle -->\n        <button\n          type=\"button\"\n          title=${expandLabel}\n          aria-label=${expandLabel}\n          aria-expanded=${this.showExtended ? 'true' : 'false'}\n          class=${cn(\n            'text-muted-foreground hover:text-foreground hover:bg-muted focus-visible:ring-ring inline-flex size-7 items-center justify-center rounded-md transition-colors focus-visible:ring-2 focus-visible:outline-none',\n            this.showExtended && 'bg-muted text-foreground',\n          )}\n          @click=${() => (this.showExtended = !this.showExtended)}\n        >\n          ${icon(ChevronDown, 'chevron-down', cn('size-3.5 transition-transform duration-200', this.showExtended && 'rotate-180'))}\n        </button>\n      </div>\n      <!-- Extended row (collapsible) -->\n      <div\n        class=${cn(\n          'grid transition-colors duration-200 ease-in-out',\n          this.showExtended ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0',\n        )}\n      >\n        <div class=\"overflow-hidden\">\n          <div class=\"flex items-center gap-0.5 border-t px-2 py-1.5\">\n            ${this.extendedItems.map((item) => this.renderItem(item))}\n          </div>\n        </div>\n      </div>\n    </div>`\n  }\n\n  render() {\n    return html`<div\n      part=\"base\"\n      data-uipkge=\"\"\n      data-slot=\"rich-text-editor\"\n      class=\"rich-text-editor rounded-lg border\"\n      @input=${stop}\n      @change=${stop}\n      @open-change=${stop}\n      @pressed-change=${stop}\n    >\n      ${this.ready ? this.renderToolbar() : nothing}\n      <!-- Editor (TipTap mounts its ProseMirror view in here) -->\n      <div\n        part=\"content\"\n        class=\"rich-text-content cursor-text overflow-y-auto px-3 py-2\"\n        style=${styleMap({ minHeight: this.minHeight })}\n        @click=${() => this.editor?.commands.focus()}\n      ></div>\n    </div>`\n  }\n}\n\ncustomElements.get('uip-rich-text-editor') || customElements.define('uip-rich-text-editor', UipRichTextEditor)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-rich-text-editor': UipRichTextEditor\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/rich-text-editor/rich-text-editor.ts"
    }
  ],
  "dependencies": [
    "@tiptap/core",
    "@tiptap/extension-link",
    "@tiptap/extension-placeholder",
    "@tiptap/extension-task-item",
    "@tiptap/extension-task-list",
    "@tiptap/extension-text-align",
    "@tiptap/extension-underline",
    "@tiptap/starter-kit",
    "lit",
    "lucide"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/button.json",
    "https://uipkge.dev/r/lit/input.json",
    "https://uipkge.dev/r/lit/lit-core.json",
    "https://uipkge.dev/r/lit/popover.json",
    "https://uipkge.dev/r/lit/separator.json",
    "https://uipkge.dev/r/lit/toggle.json"
  ]
}
