{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "video-playlist-sidebar",
  "title": "Video Playlist Sidebar",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/video-playlist-sidebar/video-playlist-sidebar.component.ts",
      "content": "// YouTube and Vimeo style video playlist drawer with active playing indicator,\n// durations, auto-play next switch, loop/shuffle controls, and active lesson summary\n// card. Port of the React block 1:1.\nimport { ChangeDetectionStrategy, Component, DestroyRef, Input, computed, inject, signal } from '@angular/core'\nimport {\n  Check,\n  CheckCircle2,\n  Clock,\n  Download,\n  FileCode2,\n  ListVideo,\n  LucideAngularModule,\n  Play,\n  PlayCircle,\n  Repeat,\n  Repeat1,\n  Search,\n  Shuffle,\n  SkipForward,\n} from 'lucide-angular'\nimport { cn } from '@/lib/utils'\nimport { UiBadgeComponent } from '@/ui/badge/badge.component'\nimport { UiButtonComponent } from '@/ui/button/button.component'\nimport {\n  UiCardComponent,\n  UiCardContentComponent,\n  UiCardDescriptionComponent,\n  UiCardFooterComponent,\n  UiCardHeaderComponent,\n  UiCardTitleComponent,\n} from '@/ui/card/card.component'\nimport { UiProgressComponent } from '@/ui/progress/progress.component'\nimport { UiSwitchComponent } from '@/ui/switch/switch.component'\n\nexport interface VideoResource {\n  id: string\n  name: string\n  size: string\n  type: 'code' | 'figma' | 'pdf'\n}\n\nexport interface PlaylistItem {\n  id: string\n  index: number\n  title: string\n  instructor: string\n  instructorRole: string\n  instructorAvatar: string\n  duration: string\n  durationSec: number\n  category: string\n  summary: string\n  isCompleted: boolean\n  thumbnailGradient: string\n  resources: VideoResource[]\n}\n\nconst initialPlaylist: PlaylistItem[] = [\n  {\n    id: 'vid-01',\n    index: 1,\n    title: '01. Introduction to Component Registries',\n    instructor: 'Marcus Vance',\n    instructorRole: 'Principal Design Engineer',\n    instructorAvatar: 'MV',\n    duration: '14:20',\n    durationSec: 860,\n    category: 'Architecture',\n    summary: 'Understanding unbundled component distribution, registry manifests, and own-your-code paradigms.',\n    isCompleted: true,\n    thumbnailGradient: 'from-slate-800 to-zinc-950',\n    resources: [\n      { id: 'res-1a', name: 'registry-schema-spec.pdf', size: '1.2 MB', type: 'pdf' },\n      { id: 'res-1b', name: 'starter-registry.json', size: '48 KB', type: 'code' },\n    ],\n  },\n  {\n    id: 'vid-02',\n    index: 2,\n    title: '02. Headless Reka UI Primitives',\n    instructor: 'Elena Rostova',\n    instructorRole: 'Staff Frontend Engineer',\n    instructorAvatar: 'ER',\n    duration: '18:45',\n    durationSec: 1125,\n    category: 'Accessibility',\n    summary: 'Composition with asChild, polymorphic slot mechanics, and ARIA state tree management.',\n    isCompleted: true,\n    thumbnailGradient: 'from-zinc-800 to-slate-950',\n    resources: [\n      { id: 'res-2a', name: 'reka-composition-cheatsheet.pdf', size: '2.4 MB', type: 'pdf' },\n      { id: 'res-2b', name: 'accessible-dialog.vue', size: '14 KB', type: 'code' },\n    ],\n  },\n  {\n    id: 'vid-03',\n    index: 3,\n    title: '03. Building with OKLCH Tokens',\n    instructor: 'Sarah Connor',\n    instructorRole: 'Design Systems Architect',\n    instructorAvatar: 'SC',\n    duration: '22:10',\n    durationSec: 1330,\n    category: 'Color Systems',\n    summary: 'Perceptually uniform color spaces, wide-gamut Display P3 gamut mapping, and dark mode contrast.',\n    isCompleted: false,\n    thumbnailGradient: 'from-slate-850 to-zinc-950',\n    resources: [\n      { id: 'res-3a', name: 'tokens.config.css', size: '14.2 KB', type: 'code' },\n      { id: 'res-3b', name: 'oklch-palette-matrix.fig', size: '4.8 MB', type: 'figma' },\n      { id: 'res-3c', name: 'apca-contrast-guide.pdf', size: '1.1 MB', type: 'pdf' },\n    ],\n  },\n  {\n    id: 'vid-04',\n    index: 4,\n    title: '04. Micro-Interactions & Spring Physics',\n    instructor: 'Alex Chen',\n    instructorRole: 'Motion Design Lead',\n    instructorAvatar: 'AC',\n    duration: '19:35',\n    durationSec: 1175,\n    category: 'Motion & Physics',\n    summary: 'Crafting tactile UI states, gesture dampening, spring curves, and 120fps hardware acceleration.',\n    isCompleted: false,\n    thumbnailGradient: 'from-zinc-800 to-zinc-950',\n    resources: [\n      { id: 'res-4a', name: 'spring-curves-preset.ts', size: '8.4 KB', type: 'code' },\n      { id: 'res-4b', name: 'gesture-lab.fig', size: '3.6 MB', type: 'figma' },\n    ],\n  },\n  {\n    id: 'vid-05',\n    index: 5,\n    title: '05. Multi-Framework Parity Testing',\n    instructor: 'Marcus Vance',\n    instructorRole: 'Principal Design Engineer',\n    instructorAvatar: 'MV',\n    duration: '16:50',\n    durationSec: 1010,\n    category: 'Testing & Quality',\n    summary: 'Automating visual regression tests and unit tests across dual Vue 3 and React 19 packages.',\n    isCompleted: false,\n    thumbnailGradient: 'from-slate-800 to-slate-950',\n    resources: [\n      { id: 'res-5a', name: 'parity-test-matrix.ts', size: '22.0 KB', type: 'code' },\n      { id: 'res-5b', name: 'playwright.config.ts', size: '6.5 KB', type: 'code' },\n    ],\n  },\n  {\n    id: 'vid-06',\n    index: 6,\n    title: '06. CI/CD Deployment to Cloudflare',\n    instructor: 'David Kim',\n    instructorRole: 'DevOps & Cloud Engineer',\n    instructorAvatar: 'DK',\n    duration: '25:15',\n    durationSec: 1515,\n    category: 'DevOps & Edge',\n    summary: 'Automating multi-framework build chains, registry sync, and edge deployment via Wrangler.',\n    isCompleted: false,\n    thumbnailGradient: 'from-zinc-850 to-zinc-950',\n    resources: [\n      { id: 'res-6a', name: 'wrangler.toml', size: '3.4 KB', type: 'code' },\n      { id: 'res-6b', name: 'deploy-pipeline.yml', size: '5.1 KB', type: 'code' },\n    ],\n  },\n]\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-video-playlist-sidebar, [ui-video-playlist-sidebar]',\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    UiCardDescriptionComponent,\n    UiCardFooterComponent,\n    UiCardHeaderComponent,\n    UiCardTitleComponent,\n    UiProgressComponent,\n    UiSwitchComponent,\n  ],\n  template: `\n    <div data-slot=\"video-playlist-sidebar\" [class]=\"rootClass\">\n      <!-- Main Playlist Control Card -->\n      <div ui-card class=\"border-border bg-card shadow-xs\">\n        <div ui-card-header class=\"space-y-3 pb-3 sm:pb-4\">\n          <!-- Badge row -->\n          <div class=\"flex flex-wrap items-center justify-between gap-2\">\n            <div class=\"flex items-center gap-2\">\n              <span ui-badge variant=\"secondary\" class=\"gap-1.5 text-xs font-semibold\">\n                <lucide-icon [img]=\"ListVideo\" class=\"text-primary size-3.5\" />\n                <span>Video Playlist</span>\n              </span>\n              <span ui-badge variant=\"outline\" class=\"text-muted-foreground gap-1 font-mono text-xs\">\n                <lucide-icon [img]=\"Clock\" class=\"size-3\" />\n                <span>1h 56m Total</span>\n              </span>\n            </div>\n            <span class=\"text-muted-foreground text-xs font-medium\">UIPKGE Masterclass Series</span>\n          </div>\n\n          <!-- Title & Subtitle -->\n          <div class=\"space-y-1\">\n            <h2 ui-card-title class=\"text-foreground text-lg font-bold tracking-tight sm:text-xl\">\n              Mastering Full-Stack UI Engineering\n            </h2>\n            <p ui-card-description class=\"text-xs\">\n              Curated playlist on headless primitives, OKLCH token systems, fluid motion physics, and registry CI\n              pipelines.\n            </p>\n          </div>\n\n          <!-- Progress bar and counter -->\n          <div class=\"bg-muted/40 border-border/70 space-y-2 rounded-lg border p-3\">\n            <div class=\"flex items-center justify-between text-xs\">\n              <span class=\"text-foreground font-semibold\">\n                <span class=\"font-mono tabular-nums\">{{ completedCount() }}</span> /\n                <span class=\"font-mono tabular-nums\">{{ totalCount() }}</span> Videos Completed\n              </span>\n              <span class=\"text-primary font-mono font-bold tabular-nums\">{{ progressPercent() }}%</span>\n            </div>\n            <ui-progress [value]=\"progressPercent()\" class=\"h-2\" />\n          </div>\n\n          <!-- Toolbar: Autoplay switch, Loop & Shuffle toggles -->\n          <div class=\"flex flex-wrap items-center justify-between gap-3 pt-1\">\n            <!-- Autoplay next switch -->\n            <div class=\"flex items-center gap-2.5\">\n              <ui-switch id=\"autoplay-switch\" [checked]=\"autoPlay()\" (checkedChange)=\"autoPlay.set($event)\" size=\"sm\" />\n              <label\n                for=\"autoplay-switch\"\n                class=\"text-foreground flex cursor-pointer items-center gap-1.5 text-xs font-medium select-none\"\n              >\n                <lucide-icon [img]=\"Play\" class=\"text-primary fill-primary size-3.5\" />\n                <span>Autoplay Next Video</span>\n              </label>\n            </div>\n\n            <!-- Loop and Shuffle Buttons -->\n            <div class=\"flex items-center gap-1.5\">\n              <button\n                ui-button\n                variant=\"ghost\"\n                size=\"sm\"\n                [class]=\"loopButtonClass()\"\n                [attr.aria-pressed]=\"isLooping()\"\n                aria-label=\"Toggle loop playlist\"\n                (click)=\"isLooping.set(!isLooping())\"\n              >\n                @if (isLooping()) {\n                  <lucide-icon [img]=\"Repeat1\" class=\"size-3.5\" />\n                } @else {\n                  <lucide-icon [img]=\"Repeat\" class=\"size-3.5\" />\n                }\n                <span>Loop</span>\n              </button>\n\n              <button\n                ui-button\n                variant=\"ghost\"\n                size=\"sm\"\n                [class]=\"shuffleButtonClass()\"\n                [attr.aria-pressed]=\"isShuffled()\"\n                aria-label=\"Toggle shuffle playlist\"\n                (click)=\"isShuffled.set(!isShuffled())\"\n              >\n                <lucide-icon [img]=\"Shuffle\" class=\"size-3.5\" />\n                <span>Shuffle</span>\n              </button>\n            </div>\n          </div>\n        </div>\n      </div>\n\n      <!-- Currently Active Video Card -->\n      <div\n        ui-card\n        class=\"border-primary/40 from-primary/5 via-card to-card relative overflow-hidden bg-gradient-to-br shadow-xs\"\n      >\n        <div class=\"bg-primary absolute top-0 right-0 left-0 h-0.5\"></div>\n        <div ui-card-header class=\"pb-3\">\n          <div class=\"flex flex-wrap items-center justify-between gap-2\">\n            <div class=\"flex items-center gap-2\">\n              <span ui-badge variant=\"default\" class=\"gap-1.5 text-xs font-semibold\">\n                <span class=\"size-1.5 animate-pulse rounded-full bg-white\"></span>\n                <span>Now Playing</span>\n              </span>\n              <span ui-badge variant=\"outline\" class=\"font-mono text-xs font-medium\">\n                Lesson {{ activeVideo().index }} of {{ totalCount() }}\n              </span>\n            </div>\n            <span class=\"text-muted-foreground font-mono text-xs font-semibold tabular-nums\">\n              {{ activeVideo().duration }}\n            </span>\n          </div>\n\n          <div class=\"space-y-1 pt-1\">\n            <h3 ui-card-title class=\"text-foreground text-base font-bold tracking-tight sm:text-lg\">\n              {{ activeVideo().title }}\n            </h3>\n            <p ui-card-description class=\"text-xs leading-relaxed\">\n              {{ activeVideo().summary }}\n            </p>\n          </div>\n\n          <!-- Instructor and Metadata -->\n          <div class=\"flex flex-wrap items-center justify-between gap-3 pt-2\">\n            <div class=\"flex items-center gap-2\">\n              <div\n                class=\"border-primary/30 bg-primary/15 text-primary flex size-7 items-center justify-center rounded-full border text-xs font-bold\"\n              >\n                {{ activeVideo().instructorAvatar }}\n              </div>\n              <div>\n                <p class=\"text-foreground text-xs font-semibold\">{{ activeVideo().instructor }}</p>\n                <p class=\"text-muted-foreground text-xs\">\n                  {{ activeVideo().instructorRole }} · {{ activeVideo().category }}\n                </p>\n              </div>\n            </div>\n\n            <!-- Video status controls -->\n            <div class=\"flex items-center gap-2\">\n              <button\n                ui-button\n                [variant]=\"activeVideo().isCompleted ? 'secondary' : 'outline'\"\n                size=\"sm\"\n                class=\"h-7.5 gap-1.5 px-2.5 text-xs font-medium\"\n                (click)=\"toggleComplete(activeVideo().id)\"\n              >\n                @if (activeVideo().isCompleted) {\n                  <lucide-icon [img]=\"CheckCircle2\" class=\"text-success size-3.5\" />\n                } @else {\n                  <lucide-icon [img]=\"Check\" class=\"size-3.5\" />\n                }\n                <span>{{ activeVideo().isCompleted ? 'Completed' : 'Mark as Watched' }}</span>\n              </button>\n\n              <button\n                ui-button\n                size=\"sm\"\n                class=\"h-7.5 gap-1.5 px-2.5 text-xs font-semibold shadow-xs\"\n                (click)=\"nextVideo()\"\n              >\n                <span>Next</span>\n                <lucide-icon [img]=\"SkipForward\" class=\"size-3.5\" />\n              </button>\n            </div>\n          </div>\n        </div>\n\n        <!-- Downloadable Lesson Exercise Files -->\n        <div ui-card-content class=\"border-border/60 bg-muted/20 border-t pt-3 pb-3.5\">\n          <div class=\"space-y-2\">\n            <div class=\"flex items-center justify-between\">\n              <p class=\"text-foreground flex items-center gap-1.5 text-xs font-bold tracking-tight uppercase\">\n                <lucide-icon [img]=\"FileCode2\" class=\"text-primary size-3.5\" />\n                <span>Lesson Exercise Files</span>\n              </p>\n              <span class=\"text-muted-foreground font-mono text-xs\">{{ activeVideo().resources.length }} Files</span>\n            </div>\n\n            <div class=\"grid grid-cols-1 gap-2 sm:grid-cols-2\">\n              @for (res of activeVideo().resources; track res.id) {\n                <div\n                  class=\"border-border/80 bg-card hover:bg-muted/40 flex items-center justify-between gap-2 rounded-lg border p-2 text-xs transition-colors\"\n                >\n                  <div class=\"flex min-w-0 items-center gap-2\">\n                    <span\n                      class=\"border-primary/20 bg-primary/10 text-primary flex size-6 shrink-0 items-center justify-center rounded font-mono text-xs font-bold\"\n                    >\n                      {{ res.type === 'code' ? 'TS' : res.type === 'figma' ? 'FIG' : 'PDF' }}\n                    </span>\n                    <div class=\"min-w-0\">\n                      <p class=\"text-foreground truncate text-xs font-medium\">{{ res.name }}</p>\n                      <p class=\"text-muted-foreground font-mono text-xs\">{{ res.size }}</p>\n                    </div>\n                  </div>\n\n                  <button\n                    ui-button\n                    aria-label=\"Download attachment\"\n                    variant=\"outline\"\n                    size=\"sm\"\n                    class=\"h-6.5 shrink-0 gap-1 px-2 text-xs font-medium\"\n                    (click)=\"handleDownload(res.id)\"\n                  >\n                    @if (downloadedAssets()[res.id]) {\n                      <lucide-icon [img]=\"Check\" class=\"text-success size-3\" />\n                    } @else {\n                      <lucide-icon [img]=\"Download\" class=\"size-3\" />\n                    }\n                    <span>{{ downloadedAssets()[res.id] ? 'Saved' : 'Get' }}</span>\n                  </button>\n                </div>\n              }\n            </div>\n          </div>\n        </div>\n      </div>\n\n      <!-- Playlist Queue & List Header -->\n      <div ui-card class=\"border-border bg-card shadow-xs\">\n        <div ui-card-header class=\"space-y-3 pb-3\">\n          <div class=\"flex flex-wrap items-center justify-between gap-2\">\n            <div class=\"flex items-center gap-2\">\n              <h3 ui-card-title class=\"text-foreground text-sm font-bold tracking-tight\">Playlist Queue</h3>\n              <span ui-badge variant=\"secondary\" class=\"font-mono text-xs\">{{ totalCount() }} Videos</span>\n            </div>\n            <span class=\"text-muted-foreground text-xs font-medium\">Click any lesson to play</span>\n          </div>\n\n          <!-- Search / Filter input -->\n          <div class=\"relative\">\n            <lucide-icon [img]=\"Search\" class=\"text-muted-foreground absolute top-2.5 left-2.5 size-3.5\" />\n            <input\n              [value]=\"searchQuery()\"\n              (input)=\"searchQuery.set($any($event.target).value)\"\n              type=\"text\"\n              placeholder=\"Filter playlist by title, instructor, or topic...\"\n              class=\"border-input bg-background placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 h-8 w-full rounded-md border pr-3 pl-8 text-xs shadow-xs outline-none focus-visible:ring-[3px]\"\n            />\n          </div>\n        </div>\n\n        <!-- Video Items List -->\n        <div ui-card-content class=\"divide-border/60 divide-y p-0\">\n          @for (video of filteredVideos(); track video.id) {\n            <div\n              [class]=\"queueItemClass(video.id)\"\n              tabindex=\"0\"\n              role=\"button\"\n              [attr.aria-label]=\"'Play ' + video.title\"\n              [attr.aria-current]=\"activeVideoId() === video.id ? 'true' : null\"\n              (click)=\"selectVideo(video.id)\"\n              (keydown.enter)=\"selectVideo(video.id)\"\n              (keydown.space)=\"$event.preventDefault(); selectVideo(video.id)\"\n            >\n              @if (activeVideoId() === video.id) {\n                <div class=\"bg-primary absolute top-2 bottom-2 left-0 w-1 rounded-r shadow-xs\"></div>\n              }\n\n              <!-- Left Section: Index / Status Indicator + Video Thumbnail -->\n              <div class=\"flex min-w-0 items-center gap-3\">\n                <div class=\"flex size-6 shrink-0 items-center justify-center\">\n                  @if (activeVideoId() === video.id) {\n                    <div class=\"flex h-3.5 w-3.5 items-end justify-center gap-0.5\" title=\"Playing\">\n                      <span class=\"bg-primary h-3 w-0.5 animate-pulse rounded-full\"></span>\n                      <span\n                        class=\"bg-primary h-4 w-0.5 animate-pulse rounded-full\"\n                        style=\"animation-delay: 150ms\"\n                      ></span>\n                      <span\n                        class=\"bg-primary h-2 w-0.5 animate-pulse rounded-full\"\n                        style=\"animation-delay: 300ms\"\n                      ></span>\n                    </div>\n                  } @else if (video.isCompleted) {\n                    <lucide-icon [img]=\"CheckCircle2\" class=\"text-success size-4 shrink-0\" />\n                  } @else {\n                    <span class=\"text-muted-foreground font-mono text-xs font-semibold tabular-nums\">\n                      {{ video.index.toString().padStart(2, '0') }}\n                    </span>\n                  }\n                </div>\n\n                <!-- 16:9 Video Thumbnail Preview -->\n                <div [class]=\"thumbnailClass(video.thumbnailGradient)\">\n                  <div class=\"absolute inset-0 flex items-center justify-center\">\n                    <lucide-icon [img]=\"PlayCircle\" [class]=\"playWatermarkClass(video.id)\" />\n                  </div>\n\n                  <div\n                    class=\"absolute right-1 bottom-1 rounded bg-black/85 px-1 py-0.5 font-mono text-xs font-semibold text-white tabular-nums backdrop-blur-xs\"\n                  >\n                    {{ video.duration }}\n                  </div>\n                </div>\n\n                <!-- Video Title & Instructor Info -->\n                <div class=\"min-w-0 space-y-0.5\">\n                  <p [class]=\"videoTitleClass(video.id)\">\n                    {{ video.title }}\n                  </p>\n                  <p class=\"text-muted-foreground truncate text-xs\">\n                    <span>{{ video.instructor }}</span>\n                    <span class=\"mx-1.5\">·</span>\n                    <span>{{ video.category }}</span>\n                  </p>\n                </div>\n              </div>\n\n              <!-- Right Section: Status Badge & Quick Watch Toggle -->\n              <div class=\"flex shrink-0 items-center gap-2\">\n                @if (activeVideoId() === video.id) {\n                  <span ui-badge variant=\"default\" class=\"hidden px-2 py-0.5 text-xs font-semibold sm:inline-flex\">\n                    Playing\n                  </span>\n                } @else if (video.isCompleted) {\n                  <span\n                    ui-badge\n                    variant=\"outline\"\n                    class=\"text-success hidden gap-1 px-1.5 py-0.5 text-xs font-medium sm:inline-flex\"\n                  >\n                    <lucide-icon [img]=\"Check\" class=\"size-3\" />\n                    <span>Watched</span>\n                  </span>\n                }\n\n                <button\n                  ui-button\n                  variant=\"ghost\"\n                  size=\"icon\"\n                  class=\"text-muted-foreground hover:text-foreground size-7 shrink-0\"\n                  [attr.aria-label]=\"video.isCompleted ? 'Mark as unwatched' : 'Mark as watched'\"\n                  (click)=\"$event.stopPropagation(); toggleComplete(video.id)\"\n                >\n                  @if (video.isCompleted) {\n                    <lucide-icon [img]=\"CheckCircle2\" class=\"text-success size-4\" />\n                  } @else if (activeVideoId() === video.id) {\n                    <lucide-icon [img]=\"Play\" class=\"text-primary size-3.5 fill-current\" />\n                  } @else {\n                    <lucide-icon [img]=\"Check\" class=\"text-muted-foreground/60 hover:text-foreground size-3.5\" />\n                  }\n                </button>\n              </div>\n            </div>\n          }\n\n          @if (filteredVideos().length === 0) {\n            <div class=\"text-muted-foreground p-8 text-center text-xs\">No lessons match your search criteria.</div>\n          }\n        </div>\n\n        <div ui-card-footer class=\"border-border/60 bg-muted/20 flex items-center justify-between border-t p-3 text-xs\">\n          <span class=\"text-muted-foreground\">Mastering Full-Stack UI Engineering</span>\n          <span class=\"text-muted-foreground font-mono font-medium tabular-nums\">\n            {{ completedCount() }}/{{ totalCount() }} Completed ({{ progressPercent() }}%)\n          </span>\n        </div>\n      </div>\n    </div>\n  `,\n})\nexport class UiVideoPlaylistSidebarComponent {\n  protected readonly Check = Check\n  protected readonly CheckCircle2 = CheckCircle2\n  protected readonly Clock = Clock\n  protected readonly Download = Download\n  protected readonly FileCode2 = FileCode2\n  protected readonly ListVideo = ListVideo\n  protected readonly Play = Play\n  protected readonly PlayCircle = PlayCircle\n  protected readonly Repeat = Repeat\n  protected readonly Repeat1 = Repeat1\n  protected readonly Search = Search\n  protected readonly Shuffle = Shuffle\n  protected readonly SkipForward = SkipForward\n\n  @Input() set initialVideoId(v: string) {\n    if (v !== undefined) this.activeVideoId.set(v)\n  }\n  @Input('class') className?: string\n\n  readonly playlist = signal<PlaylistItem[]>(initialPlaylist.map((v) => ({ ...v, resources: [...v.resources] })))\n  readonly activeVideoId = signal('vid-03')\n  readonly autoPlay = signal(true)\n  readonly isLooping = signal(false)\n  readonly isShuffled = signal(false)\n  readonly searchQuery = signal('')\n  readonly downloadedAssets = signal<Record<string, boolean>>({})\n\n  private downloadTimers: Record<string, ReturnType<typeof setTimeout>> = {}\n\n  constructor() {\n    inject(DestroyRef).onDestroy(() => {\n      for (const t of Object.values(this.downloadTimers)) clearTimeout(t)\n    })\n  }\n\n  readonly activeVideo = computed(() => {\n    const list = this.playlist()\n    return list.find((item) => item.id === this.activeVideoId()) ?? list[0]\n  })\n\n  readonly completedCount = computed(() => this.playlist().filter((item) => item.isCompleted).length)\n  readonly totalCount = computed(() => this.playlist().length)\n\n  readonly progressPercent = computed(() => {\n    const total = this.totalCount()\n    if (total === 0) return 0\n    return Math.round((this.completedCount() / total) * 100)\n  })\n\n  readonly filteredVideos = computed(() => {\n    const query = this.searchQuery().trim().toLowerCase()\n    if (!query) return this.playlist()\n    return this.playlist().filter(\n      (item) =>\n        item.title.toLowerCase().includes(query) ||\n        item.instructor.toLowerCase().includes(query) ||\n        item.category.toLowerCase().includes(query),\n    )\n  })\n\n  get rootClass(): string {\n    return cn('text-foreground mx-auto w-full max-w-2xl space-y-4', this.className)\n  }\n\n  loopButtonClass(): string {\n    return cn(\n      'h-7.5 gap-1.5 px-2.5 text-xs font-medium',\n      this.isLooping()\n        ? 'border-primary/30 bg-primary/10 text-primary border font-semibold'\n        : 'text-muted-foreground hover:text-foreground',\n    )\n  }\n\n  shuffleButtonClass(): string {\n    return cn(\n      'h-7.5 gap-1.5 px-2.5 text-xs font-medium',\n      this.isShuffled()\n        ? 'border-primary/30 bg-primary/10 text-primary border font-semibold'\n        : 'text-muted-foreground hover:text-foreground',\n    )\n  }\n\n  queueItemClass(id: string): string {\n    return cn(\n      'group relative flex cursor-pointer items-center justify-between gap-3 p-3 transition-colors duration-150',\n      this.activeVideoId() === id\n        ? 'border-primary/50 bg-primary/10 ring-primary/20 shadow-xs ring-1'\n        : 'hover:bg-muted/40',\n    )\n  }\n\n  thumbnailClass(gradient: string): string {\n    return cn(\n      'relative aspect-video w-24 shrink-0 overflow-hidden rounded-md border border-zinc-800 bg-zinc-950 bg-gradient-to-br shadow-xs sm:w-28',\n      gradient,\n    )\n  }\n\n  playWatermarkClass(id: string): string {\n    return cn(\n      'size-5 transition-transform group-hover:scale-110',\n      this.activeVideoId() === id ? 'text-primary fill-primary/20' : 'text-muted-foreground/80',\n    )\n  }\n\n  videoTitleClass(id: string): string {\n    return cn(\n      'truncate text-xs font-semibold transition-colors sm:text-sm',\n      this.activeVideoId() === id ? 'text-primary font-bold' : 'text-foreground group-hover:text-primary',\n    )\n  }\n\n  selectVideo(id: string): void {\n    this.activeVideoId.set(id)\n  }\n\n  toggleComplete(id: string): void {\n    this.playlist.update((prev) =>\n      prev.map((item) => (item.id === id ? { ...item, isCompleted: !item.isCompleted } : item)),\n    )\n  }\n\n  nextVideo(): void {\n    const list = this.playlist()\n    const currentIndex = list.findIndex((item) => item.id === this.activeVideoId())\n    if (currentIndex === -1) return\n\n    if (this.isShuffled()) {\n      const remaining = list.filter((item) => item.id !== this.activeVideoId())\n      if (remaining.length > 0) {\n        const randomIndex = Math.floor(Math.random() * remaining.length)\n        this.activeVideoId.set(remaining[randomIndex].id)\n      }\n      return\n    }\n\n    if (currentIndex < list.length - 1) {\n      this.activeVideoId.set(list[currentIndex + 1].id)\n    } else if (this.isLooping()) {\n      this.activeVideoId.set(list[0].id)\n    }\n  }\n\n  handleDownload(assetId: string): void {\n    this.downloadedAssets.update((prev) => ({ ...prev, [assetId]: true }))\n    clearTimeout(this.downloadTimers[assetId])\n    this.downloadTimers[assetId] = setTimeout(() => {\n      this.downloadedAssets.update((prev) => ({ ...prev, [assetId]: false }))\n    }, 2000)\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/video-playlist-sidebar/video-playlist-sidebar.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/video-playlist-sidebar/index.ts",
      "content": "export {\n  UiVideoPlaylistSidebarComponent,\n  type VideoResource,\n  type PlaylistItem,\n} from './video-playlist-sidebar.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/video-playlist-sidebar/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/progress.json",
    "https://uipkge.dev/r/angular/switch.json"
  ],
  "description": "YouTube and Vimeo style video playlist drawer with active playing indicator, durations, auto-play next switch, loop/shuffle controls, and active lesson summary card.",
  "categories": [
    "media",
    "app",
    "education"
  ]
}