{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "bookmark-manager-grid",
  "title": "Bookmark Manager Grid",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/bookmark-manager-grid/bookmark-manager-grid.component.ts",
      "content": "// Raindrop style visual web bookmarks and engineering resources manager with collection\n// folders, tag cloud filtering, real-time search, favorite toggles, copy link, and an add\n// bookmark modal. Port of the React block 1:1.\nimport { ChangeDetectionStrategy, Component, DestroyRef, Input, computed, inject, signal } from '@angular/core'\nimport {\n  BookOpen,\n  Bookmark,\n  Bot,\n  Check,\n  CheckCircle2,\n  Copy,\n  ExternalLink,\n  Globe,\n  Layers,\n  LucideAngularModule,\n  Palette,\n  Plus,\n  Search,\n  ShieldCheck,\n  Star,\n  Tag,\n  Type,\n  X,\n  Zap,\n} from 'lucide-angular'\nimport { cn } from '@/lib/utils'\nimport { UiBadgeComponent } from '@/ui/badge/badge.component'\nimport { UiButtonComponent } from '@/ui/button/button.component'\nimport { UiCardComponent, UiCardContentComponent } from '@/ui/card/card.component'\nimport {\n  UiDialogComponent,\n  UiDialogContentComponent,\n  UiDialogDescriptionComponent,\n  UiDialogFooterComponent,\n  UiDialogHeaderComponent,\n  UiDialogTitleComponent,\n} from '@/ui/dialog/dialog.component'\nimport { UiInputComponent } from '@/ui/input/input.component'\nimport {\n  UiSelectComponent,\n  UiSelectContentComponent,\n  UiSelectItemComponent,\n  UiSelectTriggerComponent,\n  UiSelectValueComponent,\n} from '@/ui/select/select.component'\nimport { UiSeparatorComponent } from '@/ui/separator/separator.component'\n\nexport type BookmarkCollection = 'design-systems' | 'web-perf' | 'typography' | 'ai-tools' | 'security'\n\nexport interface BookmarkItem {\n  id: string\n  title: string\n  url: string\n  domain: string\n  faviconUrl: string\n  previewImage: string\n  excerpt: string\n  collection: BookmarkCollection\n  tags: string[]\n  isFavorite: boolean\n  savedAt: string\n}\n\nexport interface CollectionMeta {\n  id: string\n  name: string\n  count: number\n  iconName: 'bookmark' | 'palette' | 'zap' | 'type' | 'bot' | 'shield' | 'star'\n}\n\nconst INITIAL_BOOKMARKS: BookmarkItem[] = [\n  {\n    id: 'bm-01',\n    title: 'OKLCH Color Picker & Converter',\n    url: 'https://oklch.com',\n    domain: 'oklch.com',\n    faviconUrl: 'https://images.unsplash.com/photo-1550684848-fac1c5b4e853?w=64&auto=format&fit=crop&q=80',\n    previewImage: 'https://images.unsplash.com/photo-1541701494587-cb58502866ab?w=800&auto=format&fit=crop&q=80',\n    excerpt:\n      'Interactive color space playground with gamut-mapping, APCA contrast ratios, and direct CSS/Tailwind OKLCH code generation.',\n    collection: 'design-systems',\n    tags: ['#oklch', '#tailwind', '#design-systems'],\n    isFavorite: true,\n    savedAt: 'Aug 18, 2026',\n  },\n  {\n    id: 'bm-02',\n    title: 'Reka UI Headless Vue Primitives',\n    url: 'https://reka-ui.com',\n    domain: 'reka-ui.com',\n    faviconUrl: 'https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=64&auto=format&fit=crop&q=80',\n    previewImage: 'https://images.unsplash.com/photo-1507238691740-187a5b1d37b8?w=800&auto=format&fit=crop&q=80',\n    excerpt:\n      'Accessible, completely unstyled UI component primitives for Vue 3 with WAI-ARIA compliant keyboard focus and composite slots.',\n    collection: 'design-systems',\n    tags: ['#reka-ui', '#vue', '#design-systems'],\n    isFavorite: true,\n    savedAt: 'Aug 15, 2026',\n  },\n  {\n    id: 'bm-03',\n    title: 'Web Vitals Performance Playbook',\n    url: 'https://web.dev/vitals',\n    domain: 'web.dev',\n    faviconUrl: 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=64&auto=format&fit=crop&q=80',\n    previewImage: 'https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=800&auto=format&fit=crop&q=80',\n    excerpt:\n      'Diagnostic field guides for optimizing Interaction to Next Paint (INP), Largest Contentful Paint (LCP), and layout stability.',\n    collection: 'web-perf',\n    tags: ['#web-vitals', '#web-perf', '#astro'],\n    isFavorite: false,\n    savedAt: 'Aug 12, 2026',\n  },\n  {\n    id: 'bm-04',\n    title: 'Typewolf Font Trends & Pairings',\n    url: 'https://typewolf.com',\n    domain: 'typewolf.com',\n    faviconUrl: 'https://images.unsplash.com/photo-1455390582262-044cdead277a?w=64&auto=format&fit=crop&q=80',\n    previewImage: 'https://images.unsplash.com/photo-1516962215378-7fa2e137ae93?w=800&auto=format&fit=crop&q=80',\n    excerpt:\n      'The definitive guide to web typography, font pairings, lookbooks, variable font loading strategies, and typographic hierarchy.',\n    collection: 'typography',\n    tags: ['#typography', '#design-systems', '#react'],\n    isFavorite: true,\n    savedAt: 'Aug 09, 2026',\n  },\n  {\n    id: 'bm-05',\n    title: 'Vercel AI SDK 3.0 Documentation',\n    url: 'https://sdk.vercel.ai/docs',\n    domain: 'sdk.vercel.ai',\n    faviconUrl: 'https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=64&auto=format&fit=crop&q=80',\n    previewImage: 'https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800&auto=format&fit=crop&q=80',\n    excerpt:\n      'Universal TypeScript library for streaming chat completions, structured JSON generation, tool calling, and multi-model routing.',\n    collection: 'ai-tools',\n    tags: ['#ai-prompt', '#react', '#astro'],\n    isFavorite: false,\n    savedAt: 'Aug 06, 2026',\n  },\n  {\n    id: 'bm-06',\n    title: 'OWASP API Security Top 10 Guide',\n    url: 'https://owasp.org/API-Security',\n    domain: 'owasp.org',\n    faviconUrl: 'https://images.unsplash.com/photo-1563986768609-322da13575f3?w=64&auto=format&fit=crop&q=80',\n    previewImage: 'https://images.unsplash.com/photo-1526374965328-7f61d4dc18c5?w=800&auto=format&fit=crop&q=80',\n    excerpt:\n      'Crucial security vectors for modern APIs including broken object authorization, rate limiting flaws, and JWT key hygiene.',\n    collection: 'security',\n    tags: ['#security', '#web-perf'],\n    isFavorite: false,\n    savedAt: 'Aug 03, 2026',\n  },\n]\n\nconst COLLECTIONS: CollectionMeta[] = [\n  { id: 'all', name: 'All Bookmarks', count: 142, iconName: 'bookmark' },\n  { id: 'design-systems', name: 'Design Systems', count: 28, iconName: 'palette' },\n  { id: 'web-perf', name: 'Web Performance', count: 19, iconName: 'zap' },\n  { id: 'typography', name: 'Typography & Fonts', count: 14, iconName: 'type' },\n  { id: 'ai-tools', name: 'AI & LLM Tools', count: 35, iconName: 'bot' },\n  { id: 'security', name: 'Security & Auth', count: 22, iconName: 'shield' },\n  { id: 'favorites', name: 'Favorites ★', count: 18, iconName: 'star' },\n]\n\nconst TAG_CLOUD = [\n  '#reka-ui',\n  '#tailwind',\n  '#oklch',\n  '#astro',\n  '#react',\n  '#vue',\n  '#typography',\n  '#web-vitals',\n  '#security',\n  '#ai-prompt',\n]\n\nconst COLLECTION_IDS: BookmarkCollection[] = ['design-systems', 'web-perf', 'typography', 'ai-tools', 'security']\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-bookmark-manager-grid, [ui-bookmark-manager-grid]',\n  standalone: true,\n  // React renders the root <div> itself: the host stays out of layout and `class` goes to the root.\n  host: { '[attr.class]': '\"contents\"' },\n  imports: [\n    LucideAngularModule,\n    UiBadgeComponent,\n    UiButtonComponent,\n    UiCardComponent,\n    UiCardContentComponent,\n    UiDialogComponent,\n    UiDialogContentComponent,\n    UiDialogDescriptionComponent,\n    UiDialogFooterComponent,\n    UiDialogHeaderComponent,\n    UiDialogTitleComponent,\n    UiInputComponent,\n    UiSelectComponent,\n    UiSelectContentComponent,\n    UiSelectItemComponent,\n    UiSelectTriggerComponent,\n    UiSelectValueComponent,\n    UiSeparatorComponent,\n  ],\n  template: `\n    <div data-slot=\"bookmark-manager-grid\" [class]=\"rootClass\">\n      <!-- Top Floating Toast Notification -->\n      @if (notificationToast()) {\n        <div\n          role=\"status\"\n          aria-live=\"polite\"\n          class=\"bg-foreground text-background animate-in fade-in slide-in-from-top-2 fixed top-6 right-6 z-50 flex items-center gap-2.5 rounded-lg px-4 py-2.5 text-xs font-medium shadow-lg transition-colors\"\n        >\n          <lucide-icon [img]=\"CheckCircle2\" class=\"text-primary size-4 shrink-0\" />\n          <span>{{ notificationToast() }}</span>\n        </div>\n      }\n\n      <!-- Header Section -->\n      <div\n        class=\"bg-card text-card-foreground border-border/80 flex flex-col gap-5 rounded-2xl border p-5 shadow-xs sm:flex-row sm:items-center sm:justify-between lg:p-6\"\n      >\n        <div class=\"space-y-1.5\">\n          <div class=\"flex flex-wrap items-center gap-2.5\">\n            <div class=\"bg-primary/10 text-primary grid size-9 place-items-center rounded-xl shadow-xs\">\n              <lucide-icon [img]=\"Bookmark\" class=\"size-5\" />\n            </div>\n            <h1 class=\"text-xl font-bold tracking-tight sm:text-2xl\">Bookmarks & Engineering Resources</h1>\n            <span ui-badge variant=\"secondary\" class=\"font-mono text-xs\">v2.1</span>\n          </div>\n          <p class=\"text-muted-foreground flex flex-wrap items-center gap-2 text-xs\">\n            <lucide-icon [img]=\"Layers\" class=\"size-3.5\" />\n            <span class=\"text-foreground font-medium\">142 bookmarks · 8 collections</span>\n            <span class=\"text-border hidden sm:inline\">|</span>\n            <span class=\"text-muted-foreground hidden sm:inline\">Raindrop Sync Active</span>\n            <span class=\"bg-success inline-block size-1.5 rounded-full\"></span>\n          </p>\n        </div>\n\n        <div class=\"flex flex-col gap-3 sm:flex-row sm:items-center\">\n          <!-- Search Input -->\n          <div class=\"relative w-full sm:w-64 lg:w-72\">\n            <lucide-icon\n              [img]=\"Search\"\n              class=\"text-muted-foreground pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2\"\n            />\n            <ui-input\n              [value]=\"searchQuery()\"\n              (valueChange)=\"searchQuery.set($event)\"\n              type=\"search\"\n              placeholder=\"Search bookmarks, domains, tags…\"\n              class=\"h-9 pr-8 pl-9 text-xs\"\n            />\n            @if (searchQuery()) {\n              <button\n                type=\"button\"\n                class=\"text-muted-foreground hover:text-foreground absolute top-1/2 right-2.5 min-h-6 -translate-y-1/2 text-xs\"\n                aria-label=\"Clear search\"\n                (click)=\"searchQuery.set('')\"\n              >\n                <lucide-icon [img]=\"X\" class=\"size-3.5\" />\n              </button>\n            }\n          </div>\n\n          <!-- Add Bookmark Button -->\n          <button\n            ui-button\n            size=\"sm\"\n            class=\"shrink-0 font-medium active:scale-[0.98]\"\n            (click)=\"isAddModalOpen.set(true)\"\n          >\n            <lucide-icon [img]=\"Plus\" class=\"size-4\" />\n            Add Bookmark\n          </button>\n        </div>\n      </div>\n\n      <!-- Mobile Horizontal Collections Bar (Visible on mobile/tablet) -->\n      <div\n        class=\"bg-card text-card-foreground border-border/80 flex items-center gap-1.5 overflow-x-auto rounded-xl border p-2 shadow-xs lg:hidden\"\n      >\n        @for (col of collections; track col.id) {\n          <button type=\"button\" [class]=\"mobileCollectionClass(col.id)\" (click)=\"selectedCollection.set(col.id)\">\n            <span>{{ col.name }}</span>\n            <span [class]=\"mobileCollectionCountClass(col.id)\">{{ col.count }}</span>\n          </button>\n        }\n      </div>\n\n      <!-- 2-Column Main Layout (Left: Sidebar w-60; Right: Grid) -->\n      <div class=\"flex flex-col gap-6 lg:flex-row lg:items-start\">\n        <!-- Left Collections Sidebar -->\n        <aside class=\"hidden w-60 shrink-0 space-y-6 lg:block\" aria-label=\"Collections and Tags\">\n          <!-- Collections List Card -->\n          <div class=\"bg-card text-card-foreground border-border/80 space-y-3 rounded-2xl border p-4 shadow-xs\">\n            <div class=\"flex items-center justify-between px-1\">\n              <h2 class=\"text-foreground text-xs font-semibold tracking-wider uppercase\">Collections</h2>\n              <span class=\"text-muted-foreground font-mono text-xs tabular-nums\">8 folders</span>\n            </div>\n\n            <nav class=\"space-y-1\" aria-label=\"Collection folders\">\n              @for (col of collections; track col.id) {\n                <button type=\"button\" [class]=\"sidebarCollectionClass(col.id)\" (click)=\"selectedCollection.set(col.id)\">\n                  <div class=\"flex items-center gap-2.5 truncate\">\n                    @switch (col.iconName) {\n                      @case ('bookmark') {\n                        <lucide-icon [img]=\"Bookmark\" class=\"size-4 shrink-0\" />\n                      }\n                      @case ('palette') {\n                        <lucide-icon [img]=\"Palette\" class=\"text-chart-1 size-4 shrink-0\" />\n                      }\n                      @case ('zap') {\n                        <lucide-icon [img]=\"Zap\" class=\"text-warning size-4 shrink-0\" />\n                      }\n                      @case ('type') {\n                        <lucide-icon [img]=\"Type\" class=\"text-info size-4 shrink-0\" />\n                      }\n                      @case ('bot') {\n                        <lucide-icon [img]=\"Bot\" class=\"text-success size-4 shrink-0\" />\n                      }\n                      @case ('shield') {\n                        <lucide-icon [img]=\"ShieldCheck\" class=\"text-destructive size-4 shrink-0\" />\n                      }\n                      @case ('star') {\n                        <lucide-icon [img]=\"Star\" class=\"fill-warning text-warning size-4 shrink-0\" />\n                      }\n                    }\n                    <span class=\"truncate\">{{ col.name }}</span>\n                  </div>\n                  <span [class]=\"sidebarCollectionCountClass(col.id)\">{{ col.count }}</span>\n                </button>\n              }\n            </nav>\n          </div>\n\n          <!-- Tags Cloud Card -->\n          <div class=\"bg-card text-card-foreground border-border/80 space-y-3 rounded-2xl border p-4 shadow-xs\">\n            <div class=\"flex items-center justify-between px-1\">\n              <h2 class=\"text-foreground flex items-center gap-1.5 text-xs font-semibold tracking-wider uppercase\">\n                <lucide-icon [img]=\"Tag\" class=\"text-primary size-3.5\" />\n                Tag Cloud\n              </h2>\n              @if (selectedTag()) {\n                <button\n                  type=\"button\"\n                  class=\"text-primary min-h-6 text-xs transition-transform duration-150 hover:underline active:scale-95\"\n                  (click)=\"selectedTag.set(null)\"\n                >\n                  Reset\n                </button>\n              }\n            </div>\n\n            <div class=\"flex flex-wrap gap-1.5\">\n              @for (tag of tagCloud; track tag) {\n                <button type=\"button\" [class]=\"tagCloudClass(tag)\" (click)=\"toggleTag(tag)\">\n                  {{ tag }}\n                </button>\n              }\n            </div>\n          </div>\n\n          <!-- Quick Storage Vault Info Box -->\n          <div class=\"bg-muted/40 border-border/70 text-card-foreground space-y-2 rounded-2xl border p-4 text-xs\">\n            <div class=\"flex items-center gap-2\">\n              <div class=\"bg-primary/10 text-primary grid size-6 place-items-center rounded-md\">\n                <lucide-icon [img]=\"Globe\" class=\"size-3.5\" />\n              </div>\n              <span class=\"text-foreground font-semibold\">Raindrop Cloud Vault</span>\n            </div>\n            <p class=\"text-muted-foreground text-xs leading-relaxed\">\n              Encrypted cross-device bookmark vault with automated favicon caching and full-text metadata indexing.\n            </p>\n          </div>\n        </aside>\n\n        <!-- Right Bookmarks Main Area -->\n        <main class=\"min-w-0 flex-1 space-y-4\">\n          <!-- Result Bar & Filter Indicators -->\n          <div\n            class=\"bg-card text-card-foreground border-border/80 flex flex-wrap items-center justify-between gap-3 rounded-xl border px-4 py-3 shadow-xs\"\n          >\n            <div class=\"flex flex-wrap items-center gap-2 text-xs\">\n              <span class=\"text-muted-foreground\">Collection:</span>\n              <span ui-badge variant=\"outline\" class=\"border-primary/40 bg-primary/5 text-primary text-xs font-medium\">\n                {{ activeCollectionMeta().name }}\n              </span>\n\n              @if (selectedTag(); as tag) {\n                <span class=\"text-muted-foreground ml-1\">Tag:</span>\n                <span ui-badge variant=\"secondary\" class=\"text-xs font-medium\">\n                  {{ tag }}\n                  <button\n                    type=\"button\"\n                    class=\"text-muted-foreground hover:text-foreground ml-1 inline-flex items-center\"\n                    aria-label=\"Remove tag filter\"\n                    (click)=\"selectedTag.set(null)\"\n                  >\n                    <lucide-icon [img]=\"X\" class=\"size-3\" />\n                  </button>\n                </span>\n              }\n\n              @if (searchQuery()) {\n                <span class=\"text-muted-foreground ml-1\">Query:</span>\n                <span class=\"text-foreground font-mono font-medium\">&ldquo;{{ searchQuery() }}&rdquo;</span>\n              }\n\n              @if (selectedCollection() !== 'all' || selectedTag() || searchQuery()) {\n                <button\n                  type=\"button\"\n                  class=\"text-muted-foreground hover:text-foreground ml-2 min-h-6 text-xs underline underline-offset-2\"\n                  (click)=\"resetFilters()\"\n                >\n                  Clear filters\n                </button>\n              }\n            </div>\n\n            <div class=\"text-muted-foreground text-xs tabular-nums\">\n              Showing <span class=\"text-foreground font-semibold\">{{ filteredBookmarks().length }}</span> of\n              {{ bookmarks().length }} items\n            </div>\n          </div>\n\n          <!-- Bookmarks Grid (2 or 3-column responsive) -->\n          @if (filteredBookmarks().length > 0) {\n            <div class=\"grid grid-cols-1 gap-4 sm:gap-5 md:grid-cols-2 xl:grid-cols-3\">\n              @for (item of filteredBookmarks(); track item.id) {\n                <div\n                  ui-card\n                  class=\"group border-border/80 bg-card hover:border-border relative flex flex-col overflow-hidden rounded-xl border p-0 shadow-xs transition-colors duration-200 hover:shadow-md\"\n                >\n                  <!-- Thumbnail Visual Preview -->\n                  <div class=\"bg-muted/40 border-border/60 relative aspect-[16/10] w-full overflow-hidden border-b\">\n                    <img\n                      [src]=\"item.previewImage\"\n                      [alt]=\"item.title\"\n                      loading=\"lazy\"\n                      class=\"size-full object-cover transition-transform duration-300 group-hover:scale-105\"\n                    />\n\n                    <!-- Gradient overlay -->\n                    <div\n                      class=\"absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-black/20 opacity-40 transition-opacity group-hover:opacity-60\"\n                    ></div>\n\n                    <!-- Domain Badge Top Left -->\n                    <div class=\"absolute top-2.5 left-2.5 flex items-center gap-1.5\">\n                      <div\n                        class=\"bg-background text-foreground border-border/80 flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium shadow-xs\"\n                      >\n                        <lucide-icon [img]=\"Globe\" class=\"text-muted-foreground size-3\" />\n                        <span class=\"font-mono text-xs\">{{ item.domain }}</span>\n                      </div>\n                    </div>\n\n                    <!-- Favorite Toggle Star Button Top Right -->\n                    <button\n                      type=\"button\"\n                      [class]=\"favoriteButtonClass(item.isFavorite)\"\n                      [title]=\"item.isFavorite ? 'Remove from favorites' : 'Add to favorites'\"\n                      [attr.aria-label]=\"item.isFavorite ? 'Remove from favorites' : 'Add to favorites'\"\n                      (click)=\"toggleFavorite(item, $event)\"\n                    >\n                      <lucide-icon [img]=\"Star\" [class]=\"favoriteIconClass(item.isFavorite)\" />\n                    </button>\n                  </div>\n\n                  <!-- Card Content Body -->\n                  <div ui-card-content class=\"flex flex-1 flex-col justify-between gap-3 p-4\">\n                    <div class=\"space-y-2\">\n                      <div class=\"flex items-start justify-between gap-2\">\n                        <a\n                          [href]=\"item.url\"\n                          target=\"_blank\"\n                          rel=\"noreferrer noopener\"\n                          class=\"group-hover:text-primary line-clamp-1 min-h-6 py-0.5 text-sm font-semibold tracking-tight transition-colors hover:underline\"\n                        >\n                          {{ item.title }}\n                        </a>\n                      </div>\n\n                      <p class=\"text-muted-foreground line-clamp-2 text-xs leading-relaxed\">{{ item.excerpt }}</p>\n                    </div>\n\n                    <!-- Tag Pills -->\n                    <div class=\"flex flex-wrap items-center gap-1.5\">\n                      @for (t of item.tags; track t) {\n                        <button type=\"button\" [class]=\"tagPillClass(t)\" (click)=\"toggleTag(t)\">\n                          {{ t }}\n                        </button>\n                      }\n                    </div>\n\n                    <!-- Card Footer Actions Row -->\n                    <div class=\"border-border/60 flex items-center justify-between border-t pt-3 text-xs\">\n                      <span class=\"text-muted-foreground font-mono text-xs tabular-nums\">{{ item.savedAt }}</span>\n\n                      <div class=\"flex items-center gap-1.5\">\n                        <button\n                          ui-button\n                          variant=\"outline\"\n                          size=\"icon-xs\"\n                          class=\"size-7 transition-transform duration-150 active:scale-95\"\n                          [title]=\"copiedId() === item.id ? 'Copied URL!' : 'Copy URL'\"\n                          [attr.aria-label]=\"copiedId() === item.id ? 'Copied URL' : 'Copy URL'\"\n                          (click)=\"copyUrl(item, $event)\"\n                        >\n                          @if (copiedId() === item.id) {\n                            <lucide-icon [img]=\"Check\" class=\"text-primary size-3.5\" />\n                          } @else {\n                            <lucide-icon [img]=\"Copy\" class=\"size-3.5\" />\n                          }\n                        </button>\n\n                        <a\n                          ui-button\n                          variant=\"secondary\"\n                          size=\"sm\"\n                          class=\"h-7 gap-1 px-2.5 text-xs font-medium active:scale-[0.98]\"\n                          [href]=\"item.url\"\n                          target=\"_blank\"\n                          rel=\"noreferrer noopener\"\n                        >\n                          <span>Open</span>\n                          <lucide-icon [img]=\"ExternalLink\" class=\"size-3\" />\n                        </a>\n                      </div>\n                    </div>\n                  </div>\n                </div>\n              }\n            </div>\n          } @else {\n            <!-- Empty State -->\n            <div\n              class=\"bg-card text-card-foreground border-border/80 flex flex-col items-center justify-center rounded-2xl border p-12 text-center shadow-xs\"\n            >\n              <div class=\"bg-muted/70 text-muted-foreground mb-3 grid size-12 place-items-center rounded-xl\">\n                <lucide-icon [img]=\"BookOpen\" class=\"size-6\" />\n              </div>\n              <h2 class=\"text-base font-semibold\">No bookmarks found</h2>\n              <p class=\"text-muted-foreground mt-1 max-w-sm text-xs\">\n                No engineering resources match your active collection, tag filter, or search query. Try clearing filters\n                or add a new bookmark.\n              </p>\n              <div class=\"mt-4 flex items-center gap-2\">\n                <button ui-button variant=\"outline\" size=\"sm\" class=\"text-xs\" (click)=\"resetFilters()\">\n                  Clear All Filters\n                </button>\n                <button ui-button size=\"sm\" class=\"text-xs\" (click)=\"isAddModalOpen.set(true)\">\n                  <lucide-icon [img]=\"Plus\" class=\"size-3.5\" />\n                  Add Bookmark\n                </button>\n              </div>\n            </div>\n          }\n        </main>\n      </div>\n\n      <!-- Add Bookmark Modal Dialog -->\n      <ui-dialog [open]=\"isAddModalOpen()\" (openChange)=\"isAddModalOpen.set($event)\">\n        <ui-dialog-content class=\"sm:max-w-md\">\n          <div ui-dialog-header class=\"text-left\">\n            <div class=\"flex items-center gap-2\">\n              <div class=\"bg-primary/10 text-primary grid size-7 place-items-center rounded-lg\">\n                <lucide-icon [img]=\"Bookmark\" class=\"size-4\" />\n              </div>\n              <h2 ui-dialog-title class=\"text-base font-bold\">Add New Bookmark</h2>\n            </div>\n            <p ui-dialog-description class=\"text-xs\">\n              Save a web resource or engineering tool with automated metadata fetching.\n            </p>\n          </div>\n\n          <form class=\"space-y-4 pt-1\" (submit)=\"handleSaveBookmark($event)\">\n            <!-- URL Input with Auto-Fetch Button -->\n            <div class=\"space-y-1.5\">\n              <label class=\"text-foreground text-xs font-medium\">Website URL</label>\n              <div class=\"flex items-center gap-2\">\n                <ui-input\n                  [value]=\"formUrl()\"\n                  (valueChange)=\"formUrl.set($event)\"\n                  type=\"url\"\n                  placeholder=\"https://astro.build or https://linear.app\"\n                  class=\"h-9 text-xs\"\n                />\n                <button\n                  ui-button\n                  type=\"button\"\n                  variant=\"outline\"\n                  size=\"sm\"\n                  class=\"h-9 shrink-0 gap-1 text-xs font-medium\"\n                  title=\"Fetch metadata from URL\"\n                  (click)=\"autoFetchMetadata()\"\n                >\n                  <lucide-icon [img]=\"Globe\" class=\"text-primary size-3.5\" />\n                  <span>Fetch</span>\n                </button>\n              </div>\n            </div>\n\n            <!-- Quick Samples Preset Bar -->\n            <div class=\"flex flex-wrap items-center gap-1.5 text-xs\">\n              <span class=\"text-muted-foreground text-xs\">Try sample:</span>\n              <button\n                type=\"button\"\n                class=\"border-border bg-muted/60 hover:bg-muted text-muted-foreground hover:text-foreground min-h-6 rounded-md border px-2 py-0.5 text-xs transition-colors\"\n                (click)=\"setPresetUrl('https://astro.build')\"\n              >\n                Astro SSG\n              </button>\n              <button\n                type=\"button\"\n                class=\"border-border bg-muted/60 hover:bg-muted text-muted-foreground hover:text-foreground min-h-6 rounded-md border px-2 py-0.5 text-xs transition-colors\"\n                (click)=\"setPresetUrl('https://linear.app/method')\"\n              >\n                Linear Method\n              </button>\n              <button\n                type=\"button\"\n                class=\"border-border bg-muted/60 hover:bg-muted text-muted-foreground hover:text-foreground min-h-6 rounded-md border px-2 py-0.5 text-xs transition-colors\"\n                (click)=\"setPresetUrl('https://github.com/features')\"\n              >\n                GitHub Guides\n              </button>\n            </div>\n\n            <ui-separator class=\"bg-border/60\" />\n\n            <!-- Title Input -->\n            <div class=\"space-y-1.5\">\n              <label class=\"text-foreground text-xs font-medium\">Bookmark Title</label>\n              <ui-input\n                [value]=\"formTitle()\"\n                (valueChange)=\"formTitle.set($event)\"\n                type=\"text\"\n                placeholder=\"e.g. Astro 4.0 Server Islands & SSG Docs\"\n                class=\"h-9 text-xs\"\n              />\n            </div>\n\n            <!-- Excerpt Description -->\n            <div class=\"space-y-1.5\">\n              <label class=\"text-foreground text-xs font-medium\">Excerpt & Notes</label>\n              <ui-input\n                [value]=\"formExcerpt()\"\n                (valueChange)=\"formExcerpt.set($event)\"\n                type=\"text\"\n                placeholder=\"Brief description or takeaway notes…\"\n                class=\"h-9 text-xs\"\n              />\n            </div>\n\n            <!-- Collection Selector -->\n            <div class=\"space-y-1.5\">\n              <label class=\"text-foreground text-xs font-medium\">Collection Folder</label>\n              <ui-select [value]=\"formCollection()\" (valueChange)=\"setFormCollection($event)\">\n                <button ui-select-trigger class=\"h-9 text-xs\">\n                  <ui-select-value placeholder=\"Select collection\" />\n                </button>\n                <ui-select-content>\n                  <ui-select-item value=\"design-systems\">Design Systems</ui-select-item>\n                  <ui-select-item value=\"web-perf\">Web Performance</ui-select-item>\n                  <ui-select-item value=\"typography\">Typography & Fonts</ui-select-item>\n                  <ui-select-item value=\"ai-tools\">AI & LLM Tools</ui-select-item>\n                  <ui-select-item value=\"security\">Security & Auth</ui-select-item>\n                </ui-select-content>\n              </ui-select>\n            </div>\n\n            <!-- Tags Input -->\n            <div class=\"space-y-1.5\">\n              <label class=\"text-foreground text-xs font-medium\">Tags (comma separated)</label>\n              <ui-input\n                [value]=\"formTags()\"\n                (valueChange)=\"formTags.set($event)\"\n                type=\"text\"\n                placeholder=\"#astro, #vue, #web-perf\"\n                class=\"h-9 text-xs\"\n              />\n            </div>\n\n            <!-- Dialog Footer Buttons -->\n            <div ui-dialog-footer class=\"gap-2 pt-2 sm:gap-0\">\n              <button\n                ui-button\n                type=\"button\"\n                variant=\"outline\"\n                size=\"sm\"\n                class=\"text-xs\"\n                (click)=\"isAddModalOpen.set(false)\"\n              >\n                Cancel\n              </button>\n              <button ui-button type=\"submit\" size=\"sm\" class=\"text-xs font-medium\">\n                <lucide-icon [img]=\"Plus\" class=\"size-3.5\" />\n                Save Bookmark\n              </button>\n            </div>\n          </form>\n        </ui-dialog-content>\n      </ui-dialog>\n    </div>\n  `,\n})\nexport class UiBookmarkManagerGridComponent {\n  protected readonly BookOpen = BookOpen\n  protected readonly Bookmark = Bookmark\n  protected readonly Bot = Bot\n  protected readonly Check = Check\n  protected readonly CheckCircle2 = CheckCircle2\n  protected readonly Copy = Copy\n  protected readonly ExternalLink = ExternalLink\n  protected readonly Globe = Globe\n  protected readonly Layers = Layers\n  protected readonly Palette = Palette\n  protected readonly Plus = Plus\n  protected readonly Search = Search\n  protected readonly ShieldCheck = ShieldCheck\n  protected readonly Star = Star\n  protected readonly Tag = Tag\n  protected readonly Type = Type\n  protected readonly X = X\n  protected readonly Zap = Zap\n\n  protected readonly collections = COLLECTIONS\n  protected readonly tagCloud = TAG_CLOUD\n\n  @Input() set initialBookmarks(v: BookmarkItem[] | undefined) {\n    if (v !== undefined) this.bookmarks.set(v.map((item) => ({ ...item, tags: [...item.tags] })))\n  }\n  @Input('class') className?: string\n\n  readonly bookmarks = signal<BookmarkItem[]>(INITIAL_BOOKMARKS.map((b) => ({ ...b, tags: [...b.tags] })))\n  readonly searchQuery = signal('')\n  readonly selectedCollection = signal('all')\n  readonly selectedTag = signal<string | null>(null)\n\n  // Add Bookmark Modal state\n  readonly isAddModalOpen = signal(false)\n  readonly formUrl = signal('')\n  readonly formTitle = signal('')\n  readonly formDomain = signal('')\n  readonly formExcerpt = signal('')\n  readonly formCollection = signal<BookmarkCollection>('design-systems')\n  readonly formTags = signal('')\n  readonly formPreview = signal('')\n\n  // Toast Notification\n  readonly notificationToast = signal<string | null>(null)\n  private toastTimer: ReturnType<typeof setTimeout> | undefined\n\n  // Copy URL feedback\n  readonly copiedId = signal<string | null>(null)\n  private copyTimer: ReturnType<typeof setTimeout> | undefined\n\n  constructor() {\n    inject(DestroyRef).onDestroy(() => {\n      clearTimeout(this.toastTimer)\n      clearTimeout(this.copyTimer)\n    })\n  }\n\n  readonly filteredBookmarks = computed(() => {\n    const collection = this.selectedCollection()\n    const tag = this.selectedTag()\n    const q = this.searchQuery().trim().toLowerCase()\n    return this.bookmarks().filter((item) => {\n      if (collection === 'favorites') {\n        if (!item.isFavorite) return false\n      } else if (collection !== 'all') {\n        if (item.collection !== collection) return false\n      }\n      if (tag && !item.tags.includes(tag)) return false\n      if (q) {\n        const titleMatch = item.title.toLowerCase().includes(q)\n        const domainMatch = item.domain.toLowerCase().includes(q)\n        const excerptMatch = item.excerpt.toLowerCase().includes(q)\n        const urlMatch = item.url.toLowerCase().includes(q)\n        const tagMatch = item.tags.some((t) => t.toLowerCase().includes(q))\n        if (!titleMatch && !domainMatch && !excerptMatch && !urlMatch && !tagMatch) return false\n      }\n      return true\n    })\n  })\n\n  readonly activeCollectionMeta = computed(\n    () => COLLECTIONS.find((c) => c.id === this.selectedCollection()) ?? COLLECTIONS[0]!,\n  )\n\n  get rootClass(): string {\n    return cn('w-full space-y-6', this.className)\n  }\n\n  mobileCollectionClass(id: string): string {\n    return cn(\n      'focus-visible:ring-ring flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-medium whitespace-nowrap transition-colors focus-visible:ring-2 focus-visible:outline-none',\n      this.selectedCollection() === id\n        ? 'bg-primary text-primary-foreground shadow-xs'\n        : 'bg-muted/60 text-muted-foreground hover:bg-muted hover:text-foreground',\n    )\n  }\n\n  mobileCollectionCountClass(id: string): string {\n    return cn(\n      'py-0.2 rounded-full px-1.5 font-mono text-xs tabular-nums',\n      this.selectedCollection() === id\n        ? 'bg-primary-foreground/20 text-primary-foreground'\n        : 'bg-background/80 text-muted-foreground',\n    )\n  }\n\n  sidebarCollectionClass(id: string): string {\n    return cn(\n      'focus-visible:ring-ring flex w-full items-center justify-between rounded-lg px-3 py-2 text-xs font-medium transition-[color,background-color,box-shadow,transform] duration-150 focus-visible:ring-2 focus-visible:outline-none active:scale-[0.98]',\n      this.selectedCollection() === id\n        ? 'bg-primary/10 text-primary border-primary/20 border font-semibold shadow-xs'\n        : 'text-muted-foreground hover:bg-muted/70 hover:text-foreground border border-transparent',\n    )\n  }\n\n  sidebarCollectionCountClass(id: string): string {\n    return cn(\n      'rounded-md px-1.5 py-0.5 font-mono text-xs tabular-nums',\n      this.selectedCollection() === id ? 'bg-primary/20 text-primary font-bold' : 'bg-muted/60 text-muted-foreground',\n    )\n  }\n\n  tagCloudClass(tag: string): string {\n    return cn(\n      'focus-visible:ring-ring min-h-6 rounded-md border px-2 py-0.5 text-xs font-medium transition-[colors,transform] duration-150 focus-visible:ring-2 focus-visible:outline-none active:scale-95',\n      this.selectedTag() === tag\n        ? 'border-primary bg-primary/10 text-primary font-semibold shadow-xs'\n        : 'border-border bg-background text-muted-foreground hover:border-foreground/30 hover:text-foreground',\n    )\n  }\n\n  tagPillClass(tag: string): string {\n    return cn(\n      'min-h-6 rounded-md px-2 py-0.5 text-xs font-medium transition-[colors,transform] duration-150 active:scale-95',\n      this.selectedTag() === tag\n        ? 'bg-primary text-primary-foreground font-semibold'\n        : 'bg-muted/70 text-muted-foreground hover:bg-muted hover:text-foreground',\n    )\n  }\n\n  favoriteButtonClass(isFavorite: boolean): string {\n    return cn(\n      'bg-background border-border/80 focus-visible:ring-ring absolute top-2.5 right-2.5 grid size-7 place-items-center rounded-full border shadow-xs transition-transform duration-150 hover:scale-105 focus-visible:ring-2 focus-visible:outline-none active:scale-90',\n      isFavorite ? 'text-warning' : 'text-muted-foreground hover:text-warning',\n    )\n  }\n\n  favoriteIconClass(isFavorite: boolean): string {\n    return cn('size-4', isFavorite ? 'fill-warning text-warning' : '')\n  }\n\n  showNotification(msg: string): void {\n    this.notificationToast.set(msg)\n    clearTimeout(this.toastTimer)\n    this.toastTimer = setTimeout(() => this.notificationToast.set(null), 2800)\n  }\n\n  async copyUrl(item: BookmarkItem, event?: MouseEvent): Promise<void> {\n    event?.stopPropagation()\n    try {\n      await navigator.clipboard?.writeText(item.url)\n    } catch {\n      // Clipboard fallback\n    }\n    this.copiedId.set(item.id)\n    this.showNotification(`Copied link to clipboard: ${item.domain}`)\n    clearTimeout(this.copyTimer)\n    this.copyTimer = setTimeout(() => this.copiedId.set(null), 2000)\n  }\n\n  toggleFavorite(item: BookmarkItem, event?: MouseEvent): void {\n    event?.stopPropagation()\n    this.bookmarks.update((prev) =>\n      prev.map((bm) => {\n        if (bm.id !== item.id) return bm\n        const nextFav = !bm.isFavorite\n        this.showNotification(nextFav ? `Starred \"${bm.title}\"` : `Removed \"${bm.title}\" from favorites`)\n        return { ...bm, isFavorite: nextFav }\n      }),\n    )\n  }\n\n  toggleTag(tag: string): void {\n    this.selectedTag.update((prev) => (prev === tag ? null : tag))\n  }\n\n  resetFilters(): void {\n    this.searchQuery.set('')\n    this.selectedCollection.set('all')\n    this.selectedTag.set(null)\n  }\n\n  setFormCollection(value: string): void {\n    if ((COLLECTION_IDS as string[]).includes(value)) this.formCollection.set(value as BookmarkCollection)\n  }\n\n  setPresetUrl(url: string): void {\n    this.formUrl.set(url)\n    this.autoFetchMetadata()\n  }\n\n  autoFetchMetadata(): void {\n    const url = this.formUrl().trim()\n    if (!url) {\n      this.showNotification('Please enter a URL first')\n      return\n    }\n\n    let extractedDomain = ''\n    try {\n      const parsed = new URL(url.startsWith('http') ? url : `https://${url}`)\n      extractedDomain = parsed.hostname.replace(/^www\\./, '')\n    } catch {\n      extractedDomain = 'developer.mozilla.org'\n    }\n\n    this.formDomain.set(extractedDomain)\n\n    if (url.includes('github') || extractedDomain.includes('github')) {\n      this.formTitle.set('GitHub Engineering Architecture Guides')\n      this.formExcerpt.set(\n        'In-depth architectural post-mortems, system scaling principles, and resilient CI/CD pipelines.',\n      )\n      this.formCollection.set('web-perf')\n      this.formTags.set('#web-perf, #security')\n      this.formPreview.set(\n        'https://images.unsplash.com/photo-1618401471353-b98afee0b2eb?w=800&auto=format&fit=crop&q=80',\n      )\n    } else if (url.includes('astro') || extractedDomain.includes('astro')) {\n      this.formTitle.set('Astro 4.0 Server Islands & SSG Docs')\n      this.formExcerpt.set(\n        'Zero-JS by default frontend framework combining island architecture with fast static HTML builds.',\n      )\n      this.formCollection.set('design-systems')\n      this.formTags.set('#astro, #react, #vue')\n      this.formPreview.set('https://images.unsplash.com/photo-1555066931-4365d14bab8c?w=800&auto=format&fit=crop&q=80')\n    } else if (url.includes('linear') || extractedDomain.includes('linear')) {\n      this.formTitle.set('Linear Method — Principles for Modern Software')\n      this.formExcerpt.set(\n        'Practices and philosophies for high-craft product execution, issue triage, and keyboard-first UI.',\n      )\n      this.formCollection.set('design-systems')\n      this.formTags.set('#design-systems, #tailwind')\n      this.formPreview.set('https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=800&auto=format&fit=crop&q=80')\n    } else {\n      if (!this.formTitle()) this.formTitle.set(`${extractedDomain.toUpperCase()} — Resource Guide`)\n      if (!this.formExcerpt())\n        this.formExcerpt.set('Curated documentation and developer tooling reference for modern engineering workflows.')\n      if (!this.formTags()) this.formTags.set('#oklch, #tailwind')\n      if (!this.formPreview())\n        this.formPreview.set(\n          'https://images.unsplash.com/photo-1526374965328-7f61d4dc18c5?w=800&auto=format&fit=crop&q=80',\n        )\n    }\n\n    this.showNotification(`Fetched metadata from ${extractedDomain}`)\n  }\n\n  handleSaveBookmark(event?: Event): void {\n    event?.preventDefault()\n    if (!this.formUrl().trim()) {\n      this.showNotification('Please enter a valid URL')\n      return\n    }\n\n    const finalTitle = this.formTitle().trim() || this.formDomain().trim() || 'Untitled Bookmark'\n    let cleanDomain = this.formDomain().trim()\n    if (!cleanDomain) {\n      try {\n        const raw = this.formUrl()\n        const parsed = new URL(raw.startsWith('http') ? raw : `https://${raw}`)\n        cleanDomain = parsed.hostname.replace(/^www\\./, '')\n      } catch {\n        cleanDomain = 'web.dev'\n      }\n    }\n\n    const rawTags = this.formTags()\n    const tagList = rawTags\n      ? rawTags\n          .split(',')\n          .map((t) => t.trim())\n          .filter(Boolean)\n          .map((t) => (t.startsWith('#') ? t : `#${t}`))\n      : ['#design-systems']\n\n    const rawUrl = this.formUrl()\n    const newBookmark: BookmarkItem = {\n      id: `bm-${Date.now()}`,\n      title: finalTitle,\n      url: rawUrl.startsWith('http') ? rawUrl : `https://${rawUrl}`,\n      domain: cleanDomain,\n      faviconUrl: 'https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=64&auto=format&fit=crop&q=80',\n      previewImage:\n        this.formPreview() ||\n        'https://images.unsplash.com/photo-1541701494587-cb58502866ab?w=800&auto=format&fit=crop&q=80',\n      excerpt: this.formExcerpt().trim() || 'Developer documentation, components, and reference guides.',\n      collection: this.formCollection(),\n      tags: tagList,\n      isFavorite: false,\n      savedAt: 'Just now',\n    }\n\n    this.bookmarks.update((prev) => [newBookmark, ...prev])\n    this.isAddModalOpen.set(false)\n\n    this.formUrl.set('')\n    this.formTitle.set('')\n    this.formDomain.set('')\n    this.formExcerpt.set('')\n    this.formTags.set('')\n    this.formPreview.set('')\n\n    this.showNotification(`Saved \"${finalTitle}\" to bookmarks!`)\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/bookmark-manager-grid/bookmark-manager-grid.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/bookmark-manager-grid/index.ts",
      "content": "export {\n  UiBookmarkManagerGridComponent,\n  type BookmarkCollection,\n  type BookmarkItem,\n  type CollectionMeta,\n} from './bookmark-manager-grid.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/bookmark-manager-grid/index.ts"
    }
  ],
  "dependencies": [
    "lucide-angular"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/badge.json",
    "https://uipkge.dev/r/angular/button.json",
    "https://uipkge.dev/r/angular/card.json",
    "https://uipkge.dev/r/angular/dialog.json",
    "https://uipkge.dev/r/angular/input.json",
    "https://uipkge.dev/r/angular/select.json",
    "https://uipkge.dev/r/angular/separator.json"
  ],
  "description": "Raindrop style visual web bookmarks and engineering resources manager with collection folders, tag cloud filtering, real-time search, favorite toggles, copy link, and add bookmark modal.",
  "categories": [
    "productivity",
    "app",
    "dashboard",
    "collaboration"
  ]
}