{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "rich-text-editor",
  "title": "Rich Text Editor",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/rich-text-editor/rich-text-editor.component.ts",
      "content": "import {\n  AfterViewInit,\n  ChangeDetectorRef,\n  Component,\n  ElementRef,\n  EventEmitter,\n  HostListener,\n  Input,\n  OnChanges,\n  OnDestroy,\n  Output,\n  SimpleChanges,\n  ViewChild,\n  ViewEncapsulation,\n  booleanAttribute,\n  inject,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport type { ControlValueAccessor } from '@angular/forms'\nimport { Editor } from '@tiptap/core'\nimport StarterKit from '@tiptap/starter-kit'\nimport Placeholder from '@tiptap/extension-placeholder'\nimport Underline from '@tiptap/extension-underline'\nimport Link from '@tiptap/extension-link'\nimport TextAlign from '@tiptap/extension-text-align'\nimport TaskList from '@tiptap/extension-task-list'\nimport TaskItem from '@tiptap/extension-task-item'\nimport { cn } from '@/lib/utils'\n\nexport type RichTextMark = 'bold' | 'italic' | 'underline' | 'strike' | 'code'\nexport type RichTextBlock = 'paragraph' | 'h1' | 'h2' | 'h3' | 'bulletList' | 'orderedList' | 'blockquote' | 'codeBlock'\n\n// Ported from RichTextEditor (React/Vue). Injected once so the component ships\n// self-contained (placeholder, prose overrides, task-list, blockquote/code/hr/link styling).\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/**\n * Angular port of UIPKGE RichTextEditor (React/Vue parity): TipTap WYSIWYG editor\n * with an essential + extended toolbar, link popover, and task lists. Same props as\n * React (`value`, `onValueChange`) and Vue (`modelValue`), plus an Angular-only\n * `disabled` extra.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-rich-text-editor, [ui-rich-text-editor]',\n  standalone: true,\n  encapsulation: ViewEncapsulation.None,\n  host: {\n    '[attr.data-slot]': '\"rich-text-editor\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-disabled]': 'disabled ? \"\" : null',\n    '[class]': 'hostClass',\n  },\n  template: `\n    <style>\n      ${richTextEditorCss}\n    </style>\n\n    <!-- Toolbar -->\n    @if (editor) {\n      <div class=\"border-b\" role=\"toolbar\" aria-label=\"Text formatting\">\n        <!-- Essential row -->\n        <div class=\"flex items-center gap-0.5 px-2 py-1.5\">\n          <!-- Bold -->\n          <button\n            type=\"button\"\n            data-uipkge=\"\"\n            data-slot=\"toggle\"\n            [attr.data-state]=\"isActive('bold') ? 'on' : 'off'\"\n            [attr.aria-pressed]=\"isActive('bold')\"\n            title=\"Bold\"\n            aria-label=\"Bold\"\n            class=\"focus-visible:ring-ring hover:bg-muted hover:text-muted-foreground inline-flex size-7 min-w-8 items-center justify-center gap-2 rounded-md bg-transparent p-0 text-sm font-medium whitespace-nowrap transition-colors duration-200 outline-none focus-visible:ring-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\"\n            [class.bg-accent]=\"isActive('bold')\"\n            [class.text-accent-foreground]=\"isActive('bold')\"\n            [disabled]=\"disabled\"\n            (click)=\"toggleMark('bold')\"\n          >\n            <svg\n              class=\"lucide lucide-bold size-3.5\"\n              aria-hidden=\"true\"\n              xmlns=\"http://www.w3.org/2000/svg\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              stroke-width=\"2\"\n              stroke-linecap=\"round\"\n              stroke-linejoin=\"round\"\n            >\n              <path d=\"M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8\" />\n            </svg>\n          </button>\n\n          <!-- Italic -->\n          <button\n            type=\"button\"\n            data-uipkge=\"\"\n            data-slot=\"toggle\"\n            [attr.data-state]=\"isActive('italic') ? 'on' : 'off'\"\n            [attr.aria-pressed]=\"isActive('italic')\"\n            title=\"Italic\"\n            aria-label=\"Italic\"\n            class=\"focus-visible:ring-ring hover:bg-muted hover:text-muted-foreground inline-flex size-7 min-w-8 items-center justify-center gap-2 rounded-md bg-transparent p-0 text-sm font-medium whitespace-nowrap transition-colors duration-200 outline-none focus-visible:ring-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\"\n            [class.bg-accent]=\"isActive('italic')\"\n            [class.text-accent-foreground]=\"isActive('italic')\"\n            [disabled]=\"disabled\"\n            (click)=\"toggleMark('italic')\"\n          >\n            <svg\n              class=\"lucide lucide-italic size-3.5\"\n              aria-hidden=\"true\"\n              xmlns=\"http://www.w3.org/2000/svg\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              stroke-width=\"2\"\n              stroke-linecap=\"round\"\n              stroke-linejoin=\"round\"\n            >\n              <line x1=\"19\" x2=\"10\" y1=\"4\" y2=\"4\" />\n              <line x1=\"14\" x2=\"5\" y1=\"20\" y2=\"20\" />\n              <line x1=\"15\" x2=\"9\" y1=\"4\" y2=\"20\" />\n            </svg>\n          </button>\n\n          <!-- Underline -->\n          <button\n            type=\"button\"\n            data-uipkge=\"\"\n            data-slot=\"toggle\"\n            [attr.data-state]=\"isActive('underline') ? 'on' : 'off'\"\n            [attr.aria-pressed]=\"isActive('underline')\"\n            title=\"Underline\"\n            aria-label=\"Underline\"\n            class=\"focus-visible:ring-ring hover:bg-muted hover:text-muted-foreground inline-flex size-7 min-w-8 items-center justify-center gap-2 rounded-md bg-transparent p-0 text-sm font-medium whitespace-nowrap transition-colors duration-200 outline-none focus-visible:ring-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\"\n            [class.bg-accent]=\"isActive('underline')\"\n            [class.text-accent-foreground]=\"isActive('underline')\"\n            [disabled]=\"disabled\"\n            (click)=\"toggleMark('underline')\"\n          >\n            <svg\n              class=\"lucide lucide-underline size-3.5\"\n              aria-hidden=\"true\"\n              xmlns=\"http://www.w3.org/2000/svg\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              stroke-width=\"2\"\n              stroke-linecap=\"round\"\n              stroke-linejoin=\"round\"\n            >\n              <path d=\"M6 4v6a6 6 0 0 0 12 0V4\" />\n              <line x1=\"4\" x2=\"20\" y1=\"20\" y2=\"20\" />\n            </svg>\n          </button>\n\n          <!-- Strikethrough -->\n          <button\n            type=\"button\"\n            data-uipkge=\"\"\n            data-slot=\"toggle\"\n            [attr.data-state]=\"isActive('strike') ? 'on' : 'off'\"\n            [attr.aria-pressed]=\"isActive('strike')\"\n            title=\"Strikethrough\"\n            aria-label=\"Strikethrough\"\n            class=\"focus-visible:ring-ring hover:bg-muted hover:text-muted-foreground inline-flex size-7 min-w-8 items-center justify-center gap-2 rounded-md bg-transparent p-0 text-sm font-medium whitespace-nowrap transition-colors duration-200 outline-none focus-visible:ring-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\"\n            [class.bg-accent]=\"isActive('strike')\"\n            [class.text-accent-foreground]=\"isActive('strike')\"\n            [disabled]=\"disabled\"\n            (click)=\"toggleMark('strike')\"\n          >\n            <svg\n              class=\"lucide lucide-strikethrough size-3.5\"\n              aria-hidden=\"true\"\n              xmlns=\"http://www.w3.org/2000/svg\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              stroke-width=\"2\"\n              stroke-linecap=\"round\"\n              stroke-linejoin=\"round\"\n            >\n              <path d=\"M16 4H9a3 3 0 0 0-2.83 4\" />\n              <path d=\"M14 12a4 4 0 0 1 0 8H6\" />\n              <line x1=\"4\" x2=\"20\" y1=\"12\" y2=\"12\" />\n            </svg>\n          </button>\n\n          <!-- Separator -->\n          <div\n            data-slot=\"separator\"\n            role=\"none\"\n            data-orientation=\"vertical\"\n            class=\"bg-border mx-1 h-5 w-px shrink-0\"\n          ></div>\n\n          <!-- Bullet list -->\n          <button\n            type=\"button\"\n            data-uipkge=\"\"\n            data-slot=\"toggle\"\n            [attr.data-state]=\"isActive('bulletList') ? 'on' : 'off'\"\n            [attr.aria-pressed]=\"isActive('bulletList')\"\n            title=\"Bullet list\"\n            aria-label=\"Bullet list\"\n            class=\"focus-visible:ring-ring hover:bg-muted hover:text-muted-foreground inline-flex size-7 min-w-8 items-center justify-center gap-2 rounded-md bg-transparent p-0 text-sm font-medium whitespace-nowrap transition-colors duration-200 outline-none focus-visible:ring-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\"\n            [class.bg-accent]=\"isActive('bulletList')\"\n            [class.text-accent-foreground]=\"isActive('bulletList')\"\n            [disabled]=\"disabled\"\n            (click)=\"setBlock('bulletList')\"\n          >\n            <svg\n              class=\"lucide lucide-list size-3.5\"\n              aria-hidden=\"true\"\n              xmlns=\"http://www.w3.org/2000/svg\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              stroke-width=\"2\"\n              stroke-linecap=\"round\"\n              stroke-linejoin=\"round\"\n            >\n              <path d=\"M3 5h.01\" />\n              <path d=\"M3 12h.01\" />\n              <path d=\"M3 19h.01\" />\n              <path d=\"M8 5h13\" />\n              <path d=\"M8 12h13\" />\n              <path d=\"M8 19h13\" />\n            </svg>\n          </button>\n\n          <!-- Numbered list -->\n          <button\n            type=\"button\"\n            data-uipkge=\"\"\n            data-slot=\"toggle\"\n            [attr.data-state]=\"isActive('orderedList') ? 'on' : 'off'\"\n            [attr.aria-pressed]=\"isActive('orderedList')\"\n            title=\"Numbered list\"\n            aria-label=\"Numbered list\"\n            class=\"focus-visible:ring-ring hover:bg-muted hover:text-muted-foreground inline-flex size-7 min-w-8 items-center justify-center gap-2 rounded-md bg-transparent p-0 text-sm font-medium whitespace-nowrap transition-colors duration-200 outline-none focus-visible:ring-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\"\n            [class.bg-accent]=\"isActive('orderedList')\"\n            [class.text-accent-foreground]=\"isActive('orderedList')\"\n            [disabled]=\"disabled\"\n            (click)=\"setBlock('orderedList')\"\n          >\n            <svg\n              class=\"lucide lucide-list-ordered size-3.5\"\n              aria-hidden=\"true\"\n              xmlns=\"http://www.w3.org/2000/svg\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              stroke-width=\"2\"\n              stroke-linecap=\"round\"\n              stroke-linejoin=\"round\"\n            >\n              <path d=\"M11 5h10\" />\n              <path d=\"M11 12h10\" />\n              <path d=\"M11 19h10\" />\n              <path d=\"M4 4h1v5\" />\n              <path d=\"M4 9h2\" />\n              <path d=\"M6.5 20H3.4c0-1 2.6-1.925 2.6-3.5a1.5 1.5 0 0 0-2.6-1.02\" />\n            </svg>\n          </button>\n\n          <!-- Separator -->\n          <div\n            data-slot=\"separator\"\n            role=\"none\"\n            data-orientation=\"vertical\"\n            class=\"bg-border mx-1 h-5 w-px shrink-0\"\n          ></div>\n\n          <!-- Link + URL popover -->\n          <div class=\"relative\">\n            <button\n              type=\"button\"\n              data-uipkge=\"\"\n              data-slot=\"toggle\"\n              [attr.data-state]=\"isActive('link') ? 'on' : 'off'\"\n              [attr.aria-pressed]=\"isActive('link')\"\n              [attr.aria-expanded]=\"linkOpen\"\n              title=\"Link\"\n              aria-label=\"Link\"\n              class=\"focus-visible:ring-ring hover:bg-muted hover:text-muted-foreground inline-flex size-7 min-w-8 items-center justify-center gap-2 rounded-md bg-transparent p-0 text-sm font-medium whitespace-nowrap transition-colors duration-200 outline-none focus-visible:ring-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\"\n              [class.bg-accent]=\"isActive('link')\"\n              [class.text-accent-foreground]=\"isActive('link')\"\n              [disabled]=\"disabled\"\n              (click)=\"openLinkEditor()\"\n            >\n              <svg\n                class=\"lucide lucide-link size-3.5\"\n                aria-hidden=\"true\"\n                xmlns=\"http://www.w3.org/2000/svg\"\n                viewBox=\"0 0 24 24\"\n                fill=\"none\"\n                stroke=\"currentColor\"\n                stroke-width=\"2\"\n                stroke-linecap=\"round\"\n                stroke-linejoin=\"round\"\n              >\n                <path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\" />\n                <path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\" />\n              </svg>\n            </button>\n            @if (linkOpen) {\n              <div\n                class=\"border-border bg-card absolute top-full left-0 z-50 mt-1 w-72 rounded-md border p-3 shadow-md\"\n                (keydown.escape)=\"closeLinkEditor(true)\"\n              >\n                <form class=\"flex flex-col gap-2\" (submit)=\"applyLink(); $event.preventDefault()\">\n                  <label [for]=\"linkFieldId\" class=\"text-foreground text-xs font-medium\">Link URL</label>\n                  <input\n                    #linkInput\n                    [id]=\"linkFieldId\"\n                    [value]=\"linkUrl\"\n                    (input)=\"linkUrl = $any($event.target).value\"\n                    type=\"url\"\n                    placeholder=\"https://example.com\"\n                    autocomplete=\"url\"\n                    spellcheck=\"false\"\n                    class=\"border-input bg-background text-foreground placeholder:text-muted-foreground focus-visible:ring-ring flex h-8 w-full rounded-md border px-2.5 text-sm focus-visible:ring-2 focus-visible:outline-none\"\n                  />\n                  <div class=\"flex items-center justify-end gap-2\">\n                    @if (isActive('link')) {\n                      <button\n                        type=\"button\"\n                        class=\"hover:bg-muted hover:text-foreground inline-flex h-8 items-center rounded-md px-3 text-xs font-medium\"\n                        (click)=\"removeLink()\"\n                      >\n                        Remove\n                      </button>\n                    }\n                    <button\n                      type=\"submit\"\n                      class=\"bg-primary text-primary-foreground hover:bg-primary/90 inline-flex h-8 items-center rounded-md px-3 text-xs font-medium disabled:pointer-events-none disabled:opacity-50\"\n                      [disabled]=\"!linkUrl.trim()\"\n                    >\n                      Apply\n                    </button>\n                  </div>\n                </form>\n              </div>\n            }\n          </div>\n\n          <!-- Separator -->\n          <div\n            data-slot=\"separator\"\n            role=\"none\"\n            data-orientation=\"vertical\"\n            class=\"bg-border mx-1 h-5 w-px shrink-0\"\n          ></div>\n\n          <!-- Undo -->\n          <button\n            type=\"button\"\n            data-uipkge=\"\"\n            data-slot=\"toggle\"\n            [attr.data-state]=\"'off'\"\n            [attr.aria-pressed]=\"false\"\n            title=\"Undo\"\n            aria-label=\"Undo\"\n            class=\"focus-visible:ring-ring hover:bg-muted hover:text-muted-foreground inline-flex size-7 min-w-8 items-center justify-center gap-2 rounded-md bg-transparent p-0 text-sm font-medium whitespace-nowrap transition-colors duration-200 outline-none focus-visible:ring-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\"\n            [disabled]=\"disabled\"\n            (click)=\"undo()\"\n          >\n            <svg\n              class=\"lucide lucide-undo-2 size-3.5\"\n              aria-hidden=\"true\"\n              xmlns=\"http://www.w3.org/2000/svg\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              stroke-width=\"2\"\n              stroke-linecap=\"round\"\n              stroke-linejoin=\"round\"\n            >\n              <path d=\"M9 14 4 9l5-5\" />\n              <path d=\"M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11\" />\n            </svg>\n          </button>\n\n          <!-- Redo -->\n          <button\n            type=\"button\"\n            data-uipkge=\"\"\n            data-slot=\"toggle\"\n            [attr.data-state]=\"'off'\"\n            [attr.aria-pressed]=\"false\"\n            title=\"Redo\"\n            aria-label=\"Redo\"\n            class=\"focus-visible:ring-ring hover:bg-muted hover:text-muted-foreground inline-flex size-7 min-w-8 items-center justify-center gap-2 rounded-md bg-transparent p-0 text-sm font-medium whitespace-nowrap transition-colors duration-200 outline-none focus-visible:ring-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\"\n            [disabled]=\"disabled\"\n            (click)=\"redo()\"\n          >\n            <svg\n              class=\"lucide lucide-redo-2 size-3.5\"\n              aria-hidden=\"true\"\n              xmlns=\"http://www.w3.org/2000/svg\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              stroke-width=\"2\"\n              stroke-linecap=\"round\"\n              stroke-linejoin=\"round\"\n            >\n              <path d=\"m15 14 5-5-5-5\" />\n              <path d=\"M20 9H9.5A5.5 5.5 0 0 0 4 14.5A5.5 5.5 0 0 0 9.5 20H13\" />\n            </svg>\n          </button>\n\n          <!-- Separator -->\n          <div\n            data-slot=\"separator\"\n            role=\"none\"\n            data-orientation=\"vertical\"\n            class=\"bg-border mx-1 h-5 w-px shrink-0\"\n          ></div>\n\n          <!-- Expand toggle -->\n          <button\n            type=\"button\"\n            [title]=\"showExtended ? 'Hide more options' : 'Show more options'\"\n            [attr.aria-label]=\"showExtended ? 'Hide more options' : 'Show more options'\"\n            [attr.aria-expanded]=\"showExtended\"\n            [class]=\"expandClass\"\n            [disabled]=\"disabled\"\n            (click)=\"toggleExtended()\"\n          >\n            <svg\n              class=\"lucide lucide-chevron-down size-3.5 transition-transform duration-200\"\n              [class.rotate-180]=\"showExtended\"\n              aria-hidden=\"true\"\n              xmlns=\"http://www.w3.org/2000/svg\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              stroke-width=\"2\"\n              stroke-linecap=\"round\"\n              stroke-linejoin=\"round\"\n            >\n              <path d=\"m6 9 6 6 6-6\" />\n            </svg>\n          </button>\n        </div>\n\n        <!-- Extended row (collapsible) -->\n        <div\n          [class]=\"extendedWrapClass\"\n        >\n          <div class=\"overflow-hidden\">\n            <div class=\"flex items-center gap-0.5 border-t px-2 py-1.5\">\n              <!-- Heading 1 -->\n              <button\n                type=\"button\"\n                data-uipkge=\"\"\n                data-slot=\"toggle\"\n                [attr.data-state]=\"isActive('h1') ? 'on' : 'off'\"\n                [attr.aria-pressed]=\"isActive('h1')\"\n                title=\"Heading 1\"\n                aria-label=\"Heading 1\"\n                class=\"focus-visible:ring-ring hover:bg-muted hover:text-muted-foreground inline-flex size-7 min-w-8 items-center justify-center gap-2 rounded-md bg-transparent p-0 text-sm font-medium whitespace-nowrap transition-colors duration-200 outline-none focus-visible:ring-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\"\n                [class.bg-accent]=\"isActive('h1')\"\n                [class.text-accent-foreground]=\"isActive('h1')\"\n                [disabled]=\"disabled\"\n                (click)=\"setBlock('h1')\"\n              >\n                <svg\n                  class=\"lucide lucide-heading-1 size-3.5\"\n                  aria-hidden=\"true\"\n                  xmlns=\"http://www.w3.org/2000/svg\"\n                  viewBox=\"0 0 24 24\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  stroke-width=\"2\"\n                  stroke-linecap=\"round\"\n                  stroke-linejoin=\"round\"\n                >\n                  <path d=\"M4 12h8\" />\n                  <path d=\"M4 18V6\" />\n                  <path d=\"M12 18V6\" />\n                  <path d=\"m17 12 3-2v8\" />\n                </svg>\n              </button>\n\n              <!-- Heading 2 -->\n              <button\n                type=\"button\"\n                data-uipkge=\"\"\n                data-slot=\"toggle\"\n                [attr.data-state]=\"isActive('h2') ? 'on' : 'off'\"\n                [attr.aria-pressed]=\"isActive('h2')\"\n                title=\"Heading 2\"\n                aria-label=\"Heading 2\"\n                class=\"focus-visible:ring-ring hover:bg-muted hover:text-muted-foreground inline-flex size-7 min-w-8 items-center justify-center gap-2 rounded-md bg-transparent p-0 text-sm font-medium whitespace-nowrap transition-colors duration-200 outline-none focus-visible:ring-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\"\n                [class.bg-accent]=\"isActive('h2')\"\n                [class.text-accent-foreground]=\"isActive('h2')\"\n                [disabled]=\"disabled\"\n                (click)=\"setBlock('h2')\"\n              >\n                <svg\n                  class=\"lucide lucide-heading-2 size-3.5\"\n                  aria-hidden=\"true\"\n                  xmlns=\"http://www.w3.org/2000/svg\"\n                  viewBox=\"0 0 24 24\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  stroke-width=\"2\"\n                  stroke-linecap=\"round\"\n                  stroke-linejoin=\"round\"\n                >\n                  <path d=\"M4 12h8\" />\n                  <path d=\"M4 18V6\" />\n                  <path d=\"M12 18V6\" />\n                  <path d=\"M21 18h-4c0-4 4-3 4-6 0-1.5-2-2.5-4-1\" />\n                </svg>\n              </button>\n\n              <!-- Separator -->\n              <div\n                data-slot=\"separator\"\n                role=\"none\"\n                data-orientation=\"vertical\"\n                class=\"bg-border mx-1 h-5 w-px shrink-0\"\n              ></div>\n\n              <!-- Inline code -->\n              <button\n                type=\"button\"\n                data-uipkge=\"\"\n                data-slot=\"toggle\"\n                [attr.data-state]=\"isActive('code') ? 'on' : 'off'\"\n                [attr.aria-pressed]=\"isActive('code')\"\n                title=\"Inline code\"\n                aria-label=\"Inline code\"\n                class=\"focus-visible:ring-ring hover:bg-muted hover:text-muted-foreground inline-flex size-7 min-w-8 items-center justify-center gap-2 rounded-md bg-transparent p-0 text-sm font-medium whitespace-nowrap transition-colors duration-200 outline-none focus-visible:ring-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\"\n                [class.bg-accent]=\"isActive('code')\"\n                [class.text-accent-foreground]=\"isActive('code')\"\n                [disabled]=\"disabled\"\n                (click)=\"toggleMark('code')\"\n              >\n                <svg\n                  class=\"lucide lucide-code size-3.5\"\n                  aria-hidden=\"true\"\n                  xmlns=\"http://www.w3.org/2000/svg\"\n                  viewBox=\"0 0 24 24\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  stroke-width=\"2\"\n                  stroke-linecap=\"round\"\n                  stroke-linejoin=\"round\"\n                >\n                  <path d=\"m16 18 6-6-6-6\" />\n                  <path d=\"m8 6-6 6 6 6\" />\n                </svg>\n              </button>\n\n              <!-- Blockquote -->\n              <button\n                type=\"button\"\n                data-uipkge=\"\"\n                data-slot=\"toggle\"\n                [attr.data-state]=\"isActive('blockquote') ? 'on' : 'off'\"\n                [attr.aria-pressed]=\"isActive('blockquote')\"\n                title=\"Blockquote\"\n                aria-label=\"Blockquote\"\n                class=\"focus-visible:ring-ring hover:bg-muted hover:text-muted-foreground inline-flex size-7 min-w-8 items-center justify-center gap-2 rounded-md bg-transparent p-0 text-sm font-medium whitespace-nowrap transition-colors duration-200 outline-none focus-visible:ring-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\"\n                [class.bg-accent]=\"isActive('blockquote')\"\n                [class.text-accent-foreground]=\"isActive('blockquote')\"\n                [disabled]=\"disabled\"\n                (click)=\"setBlock('blockquote')\"\n              >\n                <svg\n                  class=\"lucide lucide-quote size-3.5\"\n                  aria-hidden=\"true\"\n                  xmlns=\"http://www.w3.org/2000/svg\"\n                  viewBox=\"0 0 24 24\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  stroke-width=\"2\"\n                  stroke-linecap=\"round\"\n                  stroke-linejoin=\"round\"\n                >\n                  <path\n                    d=\"M16 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z\"\n                  />\n                  <path\n                    d=\"M5 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z\"\n                  />\n                </svg>\n              </button>\n\n              <!-- Divider -->\n              <button\n                type=\"button\"\n                data-uipkge=\"\"\n                data-slot=\"toggle\"\n                [attr.data-state]=\"'off'\"\n                [attr.aria-pressed]=\"false\"\n                title=\"Divider\"\n                aria-label=\"Divider\"\n                class=\"focus-visible:ring-ring hover:bg-muted hover:text-muted-foreground inline-flex size-7 min-w-8 items-center justify-center gap-2 rounded-md bg-transparent p-0 text-sm font-medium whitespace-nowrap transition-colors duration-200 outline-none focus-visible:ring-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\"\n                [disabled]=\"disabled\"\n                (click)=\"insertHr()\"\n              >\n                <svg\n                  class=\"lucide lucide-minus size-3.5\"\n                  aria-hidden=\"true\"\n                  xmlns=\"http://www.w3.org/2000/svg\"\n                  viewBox=\"0 0 24 24\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  stroke-width=\"2\"\n                  stroke-linecap=\"round\"\n                  stroke-linejoin=\"round\"\n                >\n                  <path d=\"M5 12h14\" />\n                </svg>\n              </button>\n\n              <!-- Task list -->\n              <button\n                type=\"button\"\n                data-uipkge=\"\"\n                data-slot=\"toggle\"\n                [attr.data-state]=\"isActive('taskList') ? 'on' : 'off'\"\n                [attr.aria-pressed]=\"isActive('taskList')\"\n                title=\"Task list\"\n                aria-label=\"Task list\"\n                class=\"focus-visible:ring-ring hover:bg-muted hover:text-muted-foreground inline-flex size-7 min-w-8 items-center justify-center gap-2 rounded-md bg-transparent p-0 text-sm font-medium whitespace-nowrap transition-colors duration-200 outline-none focus-visible:ring-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\"\n                [class.bg-accent]=\"isActive('taskList')\"\n                [class.text-accent-foreground]=\"isActive('taskList')\"\n                [disabled]=\"disabled\"\n                (click)=\"toggleTaskList()\"\n              >\n                <svg\n                  class=\"lucide lucide-list-checks size-3.5\"\n                  aria-hidden=\"true\"\n                  xmlns=\"http://www.w3.org/2000/svg\"\n                  viewBox=\"0 0 24 24\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  stroke-width=\"2\"\n                  stroke-linecap=\"round\"\n                  stroke-linejoin=\"round\"\n                >\n                  <path d=\"M13 5h8\" />\n                  <path d=\"M13 12h8\" />\n                  <path d=\"M13 19h8\" />\n                  <path d=\"m3 17 2 2 4-4\" />\n                  <path d=\"m3 7 2 2 4-4\" />\n                </svg>\n              </button>\n\n              <!-- Separator -->\n              <div\n                data-slot=\"separator\"\n                role=\"none\"\n                data-orientation=\"vertical\"\n                class=\"bg-border mx-1 h-5 w-px shrink-0\"\n              ></div>\n\n              <!-- Align left -->\n              <button\n                type=\"button\"\n                data-uipkge=\"\"\n                data-slot=\"toggle\"\n                [attr.data-state]=\"isActive('alignLeft') ? 'on' : 'off'\"\n                [attr.aria-pressed]=\"isActive('alignLeft')\"\n                title=\"Align left\"\n                aria-label=\"Align left\"\n                class=\"focus-visible:ring-ring hover:bg-muted hover:text-muted-foreground inline-flex size-7 min-w-8 items-center justify-center gap-2 rounded-md bg-transparent p-0 text-sm font-medium whitespace-nowrap transition-colors duration-200 outline-none focus-visible:ring-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\"\n                [class.bg-accent]=\"isActive('alignLeft')\"\n                [class.text-accent-foreground]=\"isActive('alignLeft')\"\n                [disabled]=\"disabled\"\n                (click)=\"setAlign('left')\"\n              >\n                <svg\n                  class=\"lucide lucide-align-left size-3.5\"\n                  aria-hidden=\"true\"\n                  xmlns=\"http://www.w3.org/2000/svg\"\n                  viewBox=\"0 0 24 24\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  stroke-width=\"2\"\n                  stroke-linecap=\"round\"\n                  stroke-linejoin=\"round\"\n                >\n                  <path d=\"M21 5H3\" />\n                  <path d=\"M15 12H3\" />\n                  <path d=\"M17 19H3\" />\n                </svg>\n              </button>\n\n              <!-- Align center -->\n              <button\n                type=\"button\"\n                data-uipkge=\"\"\n                data-slot=\"toggle\"\n                [attr.data-state]=\"isActive('alignCenter') ? 'on' : 'off'\"\n                [attr.aria-pressed]=\"isActive('alignCenter')\"\n                title=\"Align center\"\n                aria-label=\"Align center\"\n                class=\"focus-visible:ring-ring hover:bg-muted hover:text-muted-foreground inline-flex size-7 min-w-8 items-center justify-center gap-2 rounded-md bg-transparent p-0 text-sm font-medium whitespace-nowrap transition-colors duration-200 outline-none focus-visible:ring-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\"\n                [class.bg-accent]=\"isActive('alignCenter')\"\n                [class.text-accent-foreground]=\"isActive('alignCenter')\"\n                [disabled]=\"disabled\"\n                (click)=\"setAlign('center')\"\n              >\n                <svg\n                  class=\"lucide lucide-align-center size-3.5\"\n                  aria-hidden=\"true\"\n                  xmlns=\"http://www.w3.org/2000/svg\"\n                  viewBox=\"0 0 24 24\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  stroke-width=\"2\"\n                  stroke-linecap=\"round\"\n                  stroke-linejoin=\"round\"\n                >\n                  <path d=\"M21 5H3\" />\n                  <path d=\"M17 12H7\" />\n                  <path d=\"M19 19H5\" />\n                </svg>\n              </button>\n\n              <!-- Align right -->\n              <button\n                type=\"button\"\n                data-uipkge=\"\"\n                data-slot=\"toggle\"\n                [attr.data-state]=\"isActive('alignRight') ? 'on' : 'off'\"\n                [attr.aria-pressed]=\"isActive('alignRight')\"\n                title=\"Align right\"\n                aria-label=\"Align right\"\n                class=\"focus-visible:ring-ring hover:bg-muted hover:text-muted-foreground inline-flex size-7 min-w-8 items-center justify-center gap-2 rounded-md bg-transparent p-0 text-sm font-medium whitespace-nowrap transition-colors duration-200 outline-none focus-visible:ring-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\"\n                [class.bg-accent]=\"isActive('alignRight')\"\n                [class.text-accent-foreground]=\"isActive('alignRight')\"\n                [disabled]=\"disabled\"\n                (click)=\"setAlign('right')\"\n              >\n                <svg\n                  class=\"lucide lucide-align-right size-3.5\"\n                  aria-hidden=\"true\"\n                  xmlns=\"http://www.w3.org/2000/svg\"\n                  viewBox=\"0 0 24 24\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  stroke-width=\"2\"\n                  stroke-linecap=\"round\"\n                  stroke-linejoin=\"round\"\n                >\n                  <path d=\"M21 5H3\" />\n                  <path d=\"M21 12H9\" />\n                  <path d=\"M21 19H7\" />\n                </svg>\n              </button>\n\n              <!-- Separator -->\n              <div\n                data-slot=\"separator\"\n                role=\"none\"\n                data-orientation=\"vertical\"\n                class=\"bg-border mx-1 h-5 w-px shrink-0\"\n              ></div>\n\n              <!-- Clear formatting -->\n              <button\n                type=\"button\"\n                data-uipkge=\"\"\n                data-slot=\"toggle\"\n                [attr.data-state]=\"'off'\"\n                [attr.aria-pressed]=\"false\"\n                title=\"Clear formatting\"\n                aria-label=\"Clear formatting\"\n                class=\"focus-visible:ring-ring hover:bg-muted hover:text-muted-foreground inline-flex size-7 min-w-8 items-center justify-center gap-2 rounded-md bg-transparent p-0 text-sm font-medium whitespace-nowrap transition-colors duration-200 outline-none focus-visible:ring-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\"\n                [disabled]=\"disabled\"\n                (click)=\"clearFormatting()\"\n              >\n                <svg\n                  class=\"lucide lucide-remove-formatting size-3.5\"\n                  aria-hidden=\"true\"\n                  xmlns=\"http://www.w3.org/2000/svg\"\n                  viewBox=\"0 0 24 24\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  stroke-width=\"2\"\n                  stroke-linecap=\"round\"\n                  stroke-linejoin=\"round\"\n                >\n                  <path d=\"M4 7V4h16v3\" />\n                  <path d=\"M5 20h6\" />\n                  <path d=\"M13 4 8 20\" />\n                  <path d=\"m15 15 5 5\" />\n                  <path d=\"m20 15-5 5\" />\n                </svg>\n              </button>\n            </div>\n          </div>\n        </div>\n      </div>\n    }\n\n    <!-- Content Area -->\n    <div [class]=\"contentClass\" [style.min-height]=\"minHeight\" (click)=\"focusEditor()\">\n      <div #editorHost></div>\n    </div>\n  `,\n})\nexport class UiRichTextEditorComponent implements AfterViewInit, OnChanges, OnDestroy {\n  @Input() value?: string\n  @Input() modelValue = ''\n  @Input() placeholder = 'Start writing...'\n  @Input('class') className?: string\n  @Input() editorClass?: string\n  @Input() editorClassName?: string\n  @Input() minHeight = '120px'\n  @Input({ transform: booleanAttribute }) disabled = false\n  @Output() valueChange = new EventEmitter<string>()\n  @Output() modelValueChange = new EventEmitter<string>()\n  /** Reactive-forms hook: host apps bind a ControlValueAccessor. */\n  valueAccessor?: ControlValueAccessor\n\n  @ViewChild('editorHost') editorHost?: ElementRef<HTMLElement>\n  @ViewChild('linkInput') linkInput?: ElementRef<HTMLInputElement>\n  private cdr?: ChangeDetectorRef\n\n  constructor() {\n    try {\n      this.cdr = inject(ChangeDetectorRef, { optional: true }) ?? undefined\n    } catch {\n      // Instantiated outside injection context (e.g. unit tests)\n    }\n  }\n\n  private static nextId = 0\n\n  editor: Editor | null = null\n  showExtended = false\n  linkOpen = false\n  linkUrl = ''\n  readonly linkFieldId = `rte-link-${UiRichTextEditorComponent.nextId++}`\n  private lastInsideEvent: Event | null = null\n\n  @HostListener('click', ['$event'])\n  onHostClick(event: Event): void {\n    this.lastInsideEvent = event\n  }\n\n  @HostListener('document:click', ['$event'])\n  onDocumentClick(event: Event): void {\n    if (event === this.lastInsideEvent) {\n      this.lastInsideEvent = null\n      return\n    }\n    if (this.linkOpen) this.closeLinkEditor(false)\n  }\n\n  get currentContent(): string {\n    return this.value !== undefined ? this.value : this.modelValue\n  }\n\n  get hostClass(): string {\n    return cn(\n      'rich-text-editor block rounded-lg border',\n      this.disabled && 'opacity-50 pointer-events-none',\n      this.className,\n    )\n  }\n\n  get contentClass(): string {\n    return cn('rich-text-content cursor-text overflow-y-auto px-3 py-2', this.editorClassName ?? this.editorClass)\n  }\n\n  get expandClass(): string {\n    return 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  }\n\n  get extendedWrapClass(): string {\n    return 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\n  ngAfterViewInit(): void {\n    if (typeof window === 'undefined') return\n    const host = this.editorHost?.nativeElement\n    if (!host || this.editor) return\n    this.editor = new Editor({\n      element: host,\n      content: this.currentContent,\n      editable: !this.disabled,\n      extensions: [\n        StarterKit.configure({\n          heading: { levels: [1, 2, 3] },\n          // StarterKit ships its own link + underline since v3 — disable them so\n          // we can register the standalone packages with our own configuration\n          // without TipTap warning about duplicate extension names.\n          link: false,\n          underline: false,\n        }),\n        Placeholder.configure({ placeholder: this.placeholder }),\n        Underline,\n        Link.configure({\n          openOnClick: false,\n          HTMLAttributes: { class: 'text-primary underline cursor-pointer' },\n        }),\n        TextAlign.configure({ types: ['heading', 'paragraph'] }),\n        TaskList,\n        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 }) => this.handleUpdate(editor.getHTML()),\n    })\n    this.cdr?.detectChanges()\n  }\n\n  ngOnChanges(changes: SimpleChanges): void {\n    if (changes['disabled'] && this.editor) this.editor.setEditable(!this.disabled)\n    if ((changes['value'] || changes['modelValue']) && this.editor) {\n      const current = this.currentContent\n      if (this.editor.getHTML() !== current) {\n        this.editor.commands.setContent(current || '', { emitUpdate: false })\n      }\n    }\n  }\n\n  ngOnDestroy(): void {\n    this.editor?.destroy()\n    this.editor = null\n  }\n\n  setContent(html: string): void {\n    if (this.disabled) return\n    this.modelValue = html\n    this.value = html\n    this.editor?.commands.setContent(html, { emitUpdate: false })\n    this.modelValueChange.emit(html)\n    this.valueChange.emit(html)\n  }\n\n  handleUpdate(html: string): void {\n    this.modelValue = html\n    this.value = html\n    this.modelValueChange.emit(html)\n    this.valueChange.emit(html)\n  }\n\n  focusEditor(): void {\n    this.editor?.commands.focus()\n  }\n\n  isActive(key: string): boolean {\n    const editor = this.editor\n    if (!editor) return false\n    switch (key) {\n      case 'h1':\n        return editor.isActive('heading', { level: 1 })\n      case 'h2':\n        return editor.isActive('heading', { level: 2 })\n      case 'h3':\n        return editor.isActive('heading', { level: 3 })\n      case 'alignLeft':\n        return editor.isActive({ textAlign: 'left' })\n      case 'alignCenter':\n        return editor.isActive({ textAlign: 'center' })\n      case 'alignRight':\n        return editor.isActive({ textAlign: 'right' })\n      default:\n        return editor.isActive(key)\n    }\n  }\n\n  isMarkActive(mark: RichTextMark): boolean {\n    return this.isActive(mark)\n  }\n\n  toggleMark(mark: RichTextMark): void {\n    if (this.disabled) return\n    const editor = this.editor\n    if (!editor) return\n    switch (mark) {\n      case 'bold':\n        editor.chain().focus().toggleBold().run()\n        break\n      case 'italic':\n        editor.chain().focus().toggleItalic().run()\n        break\n      case 'underline':\n        editor.chain().focus().toggleUnderline().run()\n        break\n      case 'strike':\n        editor.chain().focus().toggleStrike().run()\n        break\n      case 'code':\n        editor.chain().focus().toggleCode().run()\n        break\n    }\n  }\n\n  setBlock(block: RichTextBlock): void {\n    if (this.disabled) return\n    const editor = this.editor\n    if (!editor) return\n    switch (block) {\n      case 'bulletList':\n        editor.chain().focus().toggleBulletList().run()\n        break\n      case 'orderedList':\n        editor.chain().focus().toggleOrderedList().run()\n        break\n      case 'h1':\n        editor.chain().focus().toggleHeading({ level: 1 }).run()\n        break\n      case 'h2':\n        editor.chain().focus().toggleHeading({ level: 2 }).run()\n        break\n      case 'h3':\n        editor.chain().focus().toggleHeading({ level: 3 }).run()\n        break\n      case 'blockquote':\n        editor.chain().focus().toggleBlockquote().run()\n        break\n      case 'codeBlock':\n        editor.chain().focus().toggleCodeBlock().run()\n        break\n      default:\n        editor.chain().focus().setParagraph().run()\n        break\n    }\n  }\n\n  setAlign(dir: 'left' | 'center' | 'right'): void {\n    if (!this.disabled) this.editor?.chain().focus().setTextAlign(dir).run()\n  }\n\n  insertHr(): void {\n    if (!this.disabled) this.editor?.chain().focus().setHorizontalRule().run()\n  }\n\n  toggleTaskList(): void {\n    if (!this.disabled) this.editor?.chain().focus().toggleTaskList().run()\n  }\n\n  clearFormatting(): void {\n    if (!this.disabled) this.editor?.chain().focus().clearNodes().unsetAllMarks().run()\n  }\n\n  undo(): void {\n    if (!this.disabled) this.editor?.chain().focus().undo().run()\n  }\n\n  redo(): void {\n    if (!this.disabled) this.editor?.chain().focus().redo().run()\n  }\n\n  toggleExtended(): void {\n    this.showExtended = !this.showExtended\n  }\n\n  openLinkEditor(): void {\n    if (this.disabled) return\n    const editor = this.editor\n    if (!editor) return\n    // Prefill with the current href so the popover edits instead of replaces.\n    this.linkUrl = (editor.getAttributes('link').href as string | undefined) ?? ''\n    this.linkOpen = true\n    setTimeout(() => this.linkInput?.nativeElement.focus(), 0)\n  }\n\n  applyLink(): void {\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  removeLink(): void {\n    this.editor?.chain().focus().extendMarkRange('link').unsetLink().run()\n    this.linkUrl = ''\n    this.linkOpen = false\n  }\n\n  closeLinkEditor(refocus = false): void {\n    this.linkOpen = false\n    if (refocus) this.editor?.commands.focus()\n  }\n\n  wordCount(): number {\n    const text = this.currentContent.replace(/<[^>]*>/g, ' ').trim()\n    if (!text) return 0\n    return text.split(/\\s+/).length\n  }\n\n  clear(): void {\n    if (!this.disabled) this.setContent('')\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/rich-text-editor/rich-text-editor.component.ts"
    },
    {
      "path": "packages/registry-angular/components/rich-text-editor/index.ts",
      "content": "export { UiRichTextEditorComponent, type RichTextMark, type RichTextBlock } from './rich-text-editor.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/rich-text-editor/index.ts"
    }
  ],
  "dependencies": [
    "@angular/forms",
    "@tiptap/core",
    "@tiptap/starter-kit",
    "@tiptap/extension-link",
    "@tiptap/extension-placeholder",
    "@tiptap/extension-task-item",
    "@tiptap/extension-task-list",
    "@tiptap/extension-text-align",
    "@tiptap/extension-underline"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "WYSIWYG editor with bold/italic/links/lists/headings/blockquote/code plus a configurable toolbar. Drop into forms where plain text is too low-level.",
  "categories": [
    "form"
  ]
}