{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "lesson-video-player",
  "title": "Lesson Video Player",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/lesson-video-player/lesson-video-player.component.ts",
      "content": "// Online lecture video player with playback speed selector, interactive timestamped\n// transcript, and note-taking drawer. Port of the React block 1:1.\nimport { ChangeDetectionStrategy, Component, Input, OnDestroy, computed, signal } from '@angular/core'\nimport {\n  ArrowRight,\n  Bookmark,\n  BookmarkCheck,\n  Check,\n  CheckCircle2,\n  ChevronRight,\n  Clock,\n  Download,\n  FileText,\n  Layers,\n  Lightbulb,\n  LucideAngularModule,\n  Maximize2,\n  MessageSquare,\n  Pause,\n  Play,\n  Plus,\n  RotateCcw,\n  RotateCw,\n  Search,\n  Subtitles,\n  ThumbsUp,\n  Trash2,\n  Volume2,\n  VolumeX,\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  UiCardHeaderComponent,\n  UiCardTitleComponent,\n} from '@/ui/card/card.component'\nimport { UiInputComponent } from '@/ui/input/input.component'\nimport { UiProgressComponent } from '@/ui/progress/progress.component'\nimport {\n  UiTabsComponent,\n  UiTabsContentComponent,\n  UiTabsListComponent,\n  UiTabsTriggerComponent,\n} from '@/ui/tabs/tabs.component'\nimport { UiTextareaComponent } from '@/ui/textarea/textarea.component'\n\nexport interface TranscriptItem {\n  id: string\n  start: number\n  end: number\n  speaker: string\n  text: string\n}\n\nexport interface NoteItem {\n  id: string\n  timestamp: number\n  text: string\n  createdAt: string\n}\n\nexport interface QuestionItem {\n  id: string\n  timestamp: number\n  author: string\n  avatar: string\n  question: string\n  upvotes: number\n  hasUpvoted?: boolean\n  answer?: { author: string; role: string; text: string; isInstructor?: boolean }\n}\n\nexport const transcriptSeed: TranscriptItem[] = [\n  {\n    id: 't-1',\n    start: 0,\n    end: 75,\n    speaker: 'Sarah Connor',\n    text: 'Welcome back! In this lesson, we are tackling one of the trickiest parts of design systems: building resilient dark-mode token palettes using the OKLCH color space.',\n  },\n  {\n    id: 't-2',\n    start: 75,\n    end: 220,\n    speaker: 'Sarah Connor',\n    text: 'Why do traditional HSL and sRGB color models fall short? Because perceptual brightness is non-uniform — pure blue at 50% lightness looks vastly darker to the human eye than pure yellow at 50% lightness.',\n  },\n  {\n    id: 't-3',\n    start: 220,\n    end: 384,\n    speaker: 'Sarah Connor',\n    text: 'OKLCH solves this by decoupling perceived lightness (L) from chroma (C) and hue (H). When you step lightness down by 10%, the perceived luminance decreases identically regardless of hue angle.',\n  },\n  {\n    id: 't-4',\n    start: 384,\n    end: 555,\n    speaker: 'Sarah Connor',\n    text: 'When calculating contrast in OKLCH, lightness is perceptually uniform across hues, which prevents dark-mode contrast crushing. Notice our formula on the slide.',\n  },\n  {\n    id: 't-5',\n    start: 555,\n    end: 750,\n    speaker: 'Sarah Connor',\n    text: 'Next, let us discuss wide-gamut Display P3 displays. While P3 provides 25% richer saturation, we must ensure automatic fallback clamping for legacy sRGB monitors.',\n  },\n  {\n    id: 't-6',\n    start: 750,\n    end: 945,\n    speaker: 'Sarah Connor',\n    text: 'In dark mode, human retinas are more sensitive to intense chroma saturation on dark surfaces. A solid heuristic is to reduce chroma by 15% to 20% on OLED dark backgrounds.',\n  },\n  {\n    id: 't-7',\n    start: 945,\n    end: 1120,\n    speaker: 'Sarah Connor',\n    text: 'To wrap up, download the starter configuration below. In the next lesson, we will wire these token variables directly into our Tailwind CSS v4 @theme inline directives.',\n  },\n]\n\nexport const notesSeed: NoteItem[] = [\n  {\n    id: 'n-1',\n    timestamp: 102,\n    text: 'Pure blue vs pure yellow in HSL: huge luminance mismatch. OKLCH fixes this at the root.',\n    createdAt: '2 hours ago',\n  },\n  {\n    id: 'n-2',\n    timestamp: 252,\n    text: 'Review color-mix in oklab formulas — can generate surface elevation tints with a single token!',\n    createdAt: '1 hour ago',\n  },\n  {\n    id: 'n-3',\n    timestamp: 384,\n    text: 'Lightness rule: Keep L >= 0.70 for primary brand text on dark surfaces to guarantee APCA Lc 60 minimum.',\n    createdAt: 'Just now',\n  },\n]\n\nexport const questionsSeed: QuestionItem[] = [\n  {\n    id: 'q-1',\n    timestamp: 225,\n    author: 'David K.',\n    avatar: 'DK',\n    question: 'Is browser support for OKLCH color-mix and raw oklch() ready for enterprise SaaS applications?',\n    upvotes: 18,\n    hasUpvoted: false,\n    answer: {\n      author: 'Sarah Connor',\n      role: 'Instructor',\n      isInstructor: true,\n      text: 'Yes! OKLCH has 98%+ global browser support across all evergreen browsers. Tailwind CSS v4 uses it as the default token format natively.',\n    },\n  },\n  {\n    id: 'q-2',\n    timestamp: 380,\n    author: 'Elena R.',\n    avatar: 'ER',\n    question: 'How do you prevent high chroma vibration on dark OLED zinc-950 surfaces?',\n    upvotes: 12,\n    hasUpvoted: false,\n    answer: {\n      author: 'Sarah Connor',\n      role: 'Instructor',\n      isInstructor: true,\n      text: 'Scale down chroma by multiplying C * 0.82 on dark tokens, while boosting lightness by +0.08 to preserve legibility without glare.',\n    },\n  },\n]\n\nexport function formatTime(seconds: number): string {\n  const m = Math.floor(seconds / 60)\n  const s = Math.floor(seconds % 60)\n  return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-lesson-video-player, [ui-lesson-video-player]',\n  standalone: true,\n  host: { '[attr.class]': '\"contents\"' },\n  imports: [\n    LucideAngularModule,\n    UiBadgeComponent,\n    UiButtonComponent,\n    UiCardComponent,\n    UiCardContentComponent,\n    UiCardDescriptionComponent,\n    UiCardHeaderComponent,\n    UiCardTitleComponent,\n    UiInputComponent,\n    UiProgressComponent,\n    UiTabsComponent,\n    UiTabsContentComponent,\n    UiTabsListComponent,\n    UiTabsTriggerComponent,\n    UiTextareaComponent,\n  ],\n  template: `\n    <div data-slot=\"lesson-video-player\" [class]=\"rootClass\">\n      <!-- Top Lesson Navigation & Course Header Bar -->\n      <header class=\"bg-card rounded-xl border p-4 shadow-xs sm:px-6 sm:py-4\">\n        <div class=\"flex flex-col gap-3 md:flex-row md:items-center md:justify-between\">\n          <div class=\"space-y-1.5\">\n            <div class=\"flex flex-wrap items-center gap-2 text-xs\">\n              <span class=\"text-muted-foreground font-medium\">Design Systems with Tailwind CSS v4</span>\n              <lucide-icon [img]=\"ChevronRight\" class=\"text-muted-foreground size-3.5\" />\n              <span ui-badge variant=\"outline\" class=\"gap-1 text-xs font-semibold\">\n                <lucide-icon [img]=\"Layers\" class=\"text-primary size-3\" />\n                <span>Module 1: Design Tokens Architecture</span>\n              </span>\n              <span class=\"text-muted-foreground hidden font-medium sm:inline\">Lesson 4 of 12</span>\n            </div>\n            <h1 class=\"text-foreground text-lg font-bold tracking-tight sm:text-xl\">\n              Lesson 4: Building Resilient OKLCH Dark-Mode Token Palettes\n            </h1>\n          </div>\n\n          <div class=\"flex flex-wrap items-center gap-2.5\">\n            <div class=\"bg-muted/40 hidden items-center gap-2 rounded-lg border px-3 py-1.5 sm:flex\">\n              <div class=\"space-y-1 text-right\">\n                <div class=\"text-muted-foreground text-xs font-medium\">Module Progress</div>\n                <div class=\"font-mono text-xs font-bold tabular-nums\">4/12 (33%)</div>\n              </div>\n              <div class=\"w-12\">\n                <ui-progress [value]=\"33\" class=\"h-1.5\" />\n              </div>\n            </div>\n\n            <button\n              ui-button\n              variant=\"outline\"\n              size=\"icon\"\n              class=\"size-8.5\"\n              [attr.aria-label]=\"isBookmarked() ? 'Remove bookmark' : 'Bookmark lesson'\"\n              (click)=\"isBookmarked.set(!isBookmarked())\"\n            >\n              @if (isBookmarked()) {\n                <lucide-icon [img]=\"BookmarkCheck\" class=\"text-primary size-4\" />\n              } @else {\n                <lucide-icon [img]=\"Bookmark\" class=\"text-muted-foreground size-4\" />\n              }\n            </button>\n\n            <button\n              ui-button\n              [variant]=\"isCompleted() ? 'default' : 'outline'\"\n              size=\"sm\"\n              class=\"h-8.5 gap-1.5 text-xs font-semibold shadow-xs\"\n              (click)=\"isCompleted.set(!isCompleted())\"\n            >\n              @if (isCompleted()) {\n                <lucide-icon [img]=\"CheckCircle2\" class=\"size-3.5\" />\n              } @else {\n                <lucide-icon [img]=\"Check\" class=\"size-3.5\" />\n              }\n              <span>{{ isCompleted() ? 'Completed' : 'Mark Complete' }}</span>\n            </button>\n\n            <button ui-button variant=\"default\" size=\"sm\" class=\"h-8.5 gap-1.5 text-xs font-semibold shadow-xs\">\n              <span>Next Lesson</span>\n              <lucide-icon [img]=\"ArrowRight\" class=\"size-3.5\" />\n            </button>\n          </div>\n        </div>\n      </header>\n\n      <!-- 2-Column Classroom Layout -->\n      <div class=\"grid grid-cols-1 gap-6 lg:grid-cols-12\">\n        <!-- Left Column: Video Player Canvas + Takeaways & Downloads -->\n        <section class=\"space-y-5 lg:col-span-8\">\n          <div\n            class=\"group relative flex min-h-[260px] w-full flex-col justify-between overflow-hidden rounded-xl border border-zinc-800 bg-zinc-950 text-white shadow-lg select-none\"\n          >\n            <div class=\"z-20 flex items-center justify-between p-3.5 sm:p-4\">\n              <div class=\"flex items-center gap-2\">\n                <span\n                  ui-badge\n                  variant=\"outline\"\n                  class=\"border-zinc-700 bg-zinc-900/80 px-2 py-0.5 font-mono text-xs font-medium text-zinc-300 backdrop-blur-md\"\n                >\n                  1080p 60fps HD\n                </span>\n                <span\n                  ui-badge\n                  variant=\"outline\"\n                  class=\"border-primary/40 bg-primary/20 text-primary-foreground px-2 py-0.5 text-xs font-semibold backdrop-blur-md\"\n                >\n                  Lesson 4 / 12\n                </span>\n              </div>\n              <div\n                class=\"flex items-center gap-2 rounded-full border border-zinc-800 bg-zinc-900/80 px-3 py-1 text-xs text-zinc-300 backdrop-blur-md\"\n              >\n                <span class=\"bg-success size-2 rounded-full\"></span>\n                <span class=\"font-medium\">Sarah Connor</span>\n                <span class=\"text-muted-foreground\">·</span>\n                <span class=\"text-muted-foreground\">Principal Design Engineer</span>\n              </div>\n            </div>\n\n            <div class=\"relative my-auto flex flex-col items-center justify-center px-6 py-4 text-center\">\n              <div\n                class=\"relative w-full max-w-xl space-y-3 rounded-lg border border-zinc-800/80 bg-zinc-900/85 p-4 text-left shadow-sm backdrop-blur-md\"\n              >\n                <div class=\"flex items-center justify-between border-b border-zinc-800 pb-2\">\n                  <div class=\"flex items-center gap-2\">\n                    <div class=\"flex gap-1.5\">\n                      <span class=\"bg-destructive/80 size-2.5 rounded-full\"></span>\n                      <span class=\"bg-warning/80 size-2.5 rounded-full\"></span>\n                      <span class=\"bg-success/80 size-2.5 rounded-full\"></span>\n                    </div>\n                    <span class=\"text-muted-foreground font-mono text-xs\"\n                      >tokens.config.css — OKLCH Palette Engine</span\n                    >\n                  </div>\n                  <span\n                    ui-badge\n                    variant=\"outline\"\n                    class=\"border-success/40 bg-success/10 text-success font-mono text-xs\"\n                  >\n                    APCA Lc 74 (AAA)\n                  </span>\n                </div>\n                <pre\n                  class=\"overflow-x-auto font-mono text-xs leading-relaxed text-zinc-300\"\n                ><code><span class=\"text-muted-foreground\">/* OKLCH Perceptual Uniformity Matrix */</span>\n<span class=\"text-chart-1\">&#64;theme inline</span> &#123;\n  <span class=\"text-info\">--color-primary</span>: <span class=\"text-warning\">oklch(0.62 0.19 259.8)</span>; <span class=\"text-muted-foreground\">/* Brand Base */</span>\n  <span class=\"text-info\">--color-surface-dark</span>: <span class=\"text-warning\">oklch(0.18 0.02 260.0)</span>; <span class=\"text-muted-foreground\">/* OLED Safe */</span>\n&#125;</code></pre>\n                <div class=\"flex items-center gap-2 pt-1\">\n                  <span class=\"text-muted-foreground text-xs font-medium\">Lightness Ramp:</span>\n                  <div class=\"flex flex-1 items-center gap-1.5\">\n                    <div class=\"h-5 flex-1 rounded bg-[oklch(0.95_0.04_259.8)] shadow-xs\" title=\"95% L\"></div>\n                    <div class=\"h-5 flex-1 rounded bg-[oklch(0.80_0.10_259.8)] shadow-xs\" title=\"80% L\"></div>\n                    <div\n                      class=\"h-5 flex-1 rounded bg-[oklch(0.62_0.19_259.8)] shadow-xs ring-1 ring-white/60\"\n                      title=\"62% L (Base)\"\n                    ></div>\n                    <div class=\"h-5 flex-1 rounded bg-[oklch(0.40_0.15_259.8)] shadow-xs\" title=\"40% L\"></div>\n                    <div class=\"h-5 flex-1 rounded bg-[oklch(0.18_0.02_259.8)] shadow-xs\" title=\"18% L (Dark)\"></div>\n                  </div>\n                </div>\n              </div>\n\n              <button\n                type=\"button\"\n                class=\"bg-primary text-primary-foreground hover:bg-primary/90 focus-visible:ring-ring absolute inset-0 m-auto flex size-14 cursor-pointer items-center justify-center rounded-full shadow-lg transition-colors focus-visible:ring-2 focus-visible:outline-none\"\n                [attr.aria-label]=\"isPlaying() ? 'Pause video' : 'Play video'\"\n                (click)=\"togglePlay()\"\n              >\n                @if (isPlaying()) {\n                  <lucide-icon [img]=\"Pause\" class=\"size-6 fill-current\" />\n                } @else {\n                  <lucide-icon [img]=\"Play\" class=\"ml-0.5 size-6 fill-current\" />\n                }\n              </button>\n            </div>\n\n            @if (showCaptions() && activeTranscriptItem()) {\n              <div\n                class=\"z-20 mx-auto mb-1 max-w-xl rounded-lg border border-white/10 bg-black/80 px-4 py-1.5 text-center text-xs font-medium text-zinc-200 shadow-md backdrop-blur-md sm:text-sm\"\n              >\n                <span class=\"text-muted-foreground\">[{{ formatTime(activeTranscriptItem()!.start) }}]</span>\n                {{ activeTranscriptItem()!.text }}\n              </div>\n            }\n\n            <div\n              class=\"z-20 space-y-2 bg-gradient-to-t from-black/95 via-black/85 to-transparent p-3 sm:px-4 sm:pt-4 sm:pb-3.5\"\n            >\n              <div\n                class=\"group/bar relative flex h-4 w-full cursor-pointer items-center\"\n                role=\"slider\"\n                aria-label=\"Video Timeline Scrubber\"\n                [attr.aria-valuenow]=\"currentTime()\"\n                aria-valuemin=\"0\"\n                [attr.aria-valuemax]=\"totalDuration()\"\n                (click)=\"handleSeekClick($event)\"\n              >\n                <div\n                  class=\"relative h-1.5 w-full overflow-hidden rounded-full bg-zinc-800 transition-[height] group-hover/bar:h-2.5\"\n                >\n                  <div\n                    class=\"absolute top-0 bottom-0 left-0 bg-zinc-700 transition-colors\"\n                    [style.width.%]=\"bufferedPercent()\"\n                  ></div>\n                  <div\n                    class=\"bg-primary absolute top-0 bottom-0 left-0 transition-colors\"\n                    [style.width.%]=\"progressPercent()\"\n                  ></div>\n                </div>\n                <div\n                  class=\"bg-primary absolute size-3.5 -translate-x-1/2 rounded-full shadow-md ring-2 ring-white transition-transform group-hover/bar:scale-125\"\n                  [style.left.%]=\"progressPercent()\"\n                ></div>\n              </div>\n\n              <div class=\"flex items-center justify-between gap-2 pt-0.5\">\n                <div class=\"flex items-center gap-1 sm:gap-2\">\n                  <button\n                    ui-button\n                    variant=\"ghost\"\n                    size=\"icon\"\n                    class=\"size-8 text-white hover:bg-white/15\"\n                    [attr.aria-label]=\"isPlaying() ? 'Pause' : 'Play'\"\n                    (click)=\"togglePlay()\"\n                  >\n                    @if (isPlaying()) {\n                      <lucide-icon [img]=\"Pause\" class=\"size-4 fill-current\" />\n                    } @else {\n                      <lucide-icon [img]=\"Play\" class=\"size-4 fill-current\" />\n                    }\n                  </button>\n                  <button\n                    ui-button\n                    variant=\"ghost\"\n                    size=\"icon\"\n                    class=\"size-8 text-zinc-300 hover:bg-white/15 hover:text-white\"\n                    aria-label=\"Rewind 15 seconds\"\n                    (click)=\"skipSeconds(-15)\"\n                  >\n                    <lucide-icon [img]=\"RotateCcw\" class=\"size-4\" />\n                  </button>\n                  <button\n                    ui-button\n                    variant=\"ghost\"\n                    size=\"icon\"\n                    class=\"size-8 text-zinc-300 hover:bg-white/15 hover:text-white\"\n                    aria-label=\"Fast forward 15 seconds\"\n                    (click)=\"skipSeconds(15)\"\n                  >\n                    <lucide-icon [img]=\"RotateCw\" class=\"size-4\" />\n                  </button>\n                  <button\n                    ui-button\n                    variant=\"ghost\"\n                    size=\"icon\"\n                    class=\"size-8 text-zinc-300 hover:bg-white/15 hover:text-white\"\n                    [attr.aria-label]=\"isMuted() ? 'Unmute' : 'Mute'\"\n                    (click)=\"toggleMute()\"\n                  >\n                    @if (isMuted()) {\n                      <lucide-icon [img]=\"VolumeX\" class=\"text-destructive size-4\" />\n                    } @else {\n                      <lucide-icon [img]=\"Volume2\" class=\"size-4\" />\n                    }\n                  </button>\n                  <div\n                    class=\"flex items-center gap-1 font-mono text-xs font-semibold text-zinc-300 tabular-nums sm:ml-1\"\n                  >\n                    <span class=\"text-white\">{{ formattedCurrentTime() }}</span>\n                    <span class=\"text-muted-foreground\">/</span>\n                    <span>{{ formattedTotalTime() }}</span>\n                  </div>\n                </div>\n                <div class=\"flex items-center gap-1 sm:gap-2\">\n                  <button\n                    ui-button\n                    variant=\"ghost\"\n                    size=\"sm\"\n                    class=\"h-8 gap-1 px-2 font-mono text-xs text-zinc-200 hover:bg-white/15 hover:text-white\"\n                    [attr.aria-label]=\"'Playback speed ' + playbackSpeed() + 'x'\"\n                    (click)=\"cycleSpeed()\"\n                  >\n                    {{ playbackSpeed() }}x\n                  </button>\n                  <button\n                    ui-button\n                    variant=\"ghost\"\n                    size=\"icon\"\n                    class=\"size-8 text-zinc-300 hover:bg-white/15 hover:text-white\"\n                    [attr.aria-label]=\"showCaptions() ? 'Hide captions' : 'Show captions'\"\n                    (click)=\"showCaptions.set(!showCaptions())\"\n                  >\n                    <lucide-icon [img]=\"Subtitles\" class=\"size-4\" />\n                  </button>\n                  <button\n                    ui-button\n                    variant=\"ghost\"\n                    size=\"icon\"\n                    class=\"size-8 text-zinc-300 hover:bg-white/15 hover:text-white\"\n                    aria-label=\"Toggle fullscreen\"\n                    (click)=\"isFullscreen.set(!isFullscreen())\"\n                  >\n                    <lucide-icon [img]=\"Maximize2\" class=\"size-4\" />\n                  </button>\n                </div>\n              </div>\n            </div>\n          </div>\n\n          <!-- Key Takeaways & Downloads -->\n          <div ui-card>\n            <div ui-card-header>\n              <div class=\"flex items-center gap-2\">\n                <lucide-icon [img]=\"Lightbulb\" class=\"text-warning size-4\" />\n                <h3 ui-card-title class=\"text-sm font-semibold\">Key Takeaways &amp; Starter Files</h3>\n              </div>\n              <p ui-card-description class=\"text-xs\">Download the OKLCH token starter configuration for this lesson.</p>\n            </div>\n            <div ui-card-content class=\"space-y-3\">\n              <ul class=\"text-muted-foreground space-y-2 text-xs sm:text-sm\">\n                <li class=\"flex items-start gap-2\">\n                  <lucide-icon [img]=\"CheckCircle2\" class=\"text-primary mt-0.5 size-4 shrink-0\" />\n                  <span\n                    >OKLCH lightness is perceptually uniform — step L by 10% and every hue darkens identically.</span\n                  >\n                </li>\n                <li class=\"flex items-start gap-2\">\n                  <lucide-icon [img]=\"CheckCircle2\" class=\"text-primary mt-0.5 size-4 shrink-0\" />\n                  <span>Reduce chroma 15–20% on OLED dark surfaces to stop high-chroma vibration and glare.</span>\n                </li>\n                <li class=\"flex items-start gap-2\">\n                  <lucide-icon [img]=\"CheckCircle2\" class=\"text-primary mt-0.5 size-4 shrink-0\" />\n                  <span>Keep L ≥ 0.70 for primary brand text on dark surfaces to guarantee APCA Lc 60 minimum.</span>\n                </li>\n              </ul>\n              <div class=\"flex flex-wrap items-center gap-2\">\n                <button\n                  ui-button\n                  variant=\"outline\"\n                  size=\"sm\"\n                  class=\"gap-1.5 text-xs\"\n                  (click)=\"handleCopyAsset('tokens')\"\n                >\n                  @if (copiedAsset() === 'tokens') {\n                    <lucide-icon [img]=\"Check\" class=\"text-success size-3.5\" />\n                    <span>Copied!</span>\n                  } @else {\n                    <lucide-icon [img]=\"FileText\" class=\"size-3.5\" />\n                    <span>tokens.config.css</span>\n                  }\n                </button>\n                <button\n                  ui-button\n                  variant=\"outline\"\n                  size=\"sm\"\n                  class=\"gap-1.5 text-xs\"\n                  (click)=\"handleCopyAsset('figma')\"\n                >\n                  @if (copiedAsset() === 'figma') {\n                    <lucide-icon [img]=\"Check\" class=\"text-success size-3.5\" />\n                    <span>Copied!</span>\n                  } @else {\n                    <lucide-icon [img]=\"Download\" class=\"size-3.5\" />\n                    <span>Figma Variables (.json)</span>\n                  }\n                </button>\n              </div>\n            </div>\n          </div>\n        </section>\n\n        <!-- Right Column: Transcript / Notes / Q&A Tabs -->\n        <aside class=\"lg:col-span-4\">\n          <div ui-card class=\"flex max-h-[860px] min-h-0 flex-col overflow-hidden\">\n            <div ui-card-header class=\"border-border border-b p-4 pb-3\">\n              <div class=\"flex items-center justify-between\">\n                <h3 ui-card-title class=\"text-sm font-semibold\">Lesson Companion</h3>\n                <span ui-badge variant=\"secondary\" class=\"font-mono text-xs tabular-nums\">\n                  {{ formattedCurrentTime() }} / {{ formattedTotalTime() }}\n                </span>\n              </div>\n            </div>\n            <ui-tabs\n              [value]=\"activeTab()\"\n              (valueChange)=\"activeTab.set($any($event))\"\n              class=\"flex min-h-0 flex-1 flex-col\"\n            >\n              <div ui-tabs-list class=\"mx-4 mt-3 grid w-auto grid-cols-3\">\n                <button ui-tabs-trigger value=\"transcript\" class=\"gap-1 text-xs\">\n                  <lucide-icon [img]=\"MessageSquare\" class=\"size-3.5\" />\n                  <span>Transcript</span>\n                </button>\n                <button ui-tabs-trigger value=\"notes\" class=\"gap-1 text-xs\">\n                  <lucide-icon [img]=\"FileText\" class=\"size-3.5\" />\n                  <span>Notes ({{ notesList().length }})</span>\n                </button>\n                <button ui-tabs-trigger value=\"qa\" class=\"gap-1 text-xs\">\n                  <lucide-icon [img]=\"ThumbsUp\" class=\"size-3.5\" />\n                  <span>Q&amp;A</span>\n                </button>\n              </div>\n\n              <div ui-tabs-content value=\"transcript\" class=\"flex min-h-0 flex-1 flex-col gap-3 p-4 outline-none\">\n                <div class=\"relative\">\n                  <lucide-icon\n                    [img]=\"Search\"\n                    class=\"text-muted-foreground pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2\"\n                  />\n                  <ui-input\n                    [value]=\"transcriptSearch()\"\n                    (valueChange)=\"transcriptSearch.set($event)\"\n                    placeholder=\"Search transcript...\"\n                    class=\"h-8 pl-8 text-xs\"\n                  />\n                </div>\n                <div class=\"min-h-0 flex-1 space-y-1 overflow-auto\">\n                  @for (item of filteredTranscript(); track item.id) {\n                    <button type=\"button\" [class]=\"transcriptRowClass(item)\" (click)=\"seekTo(item.start)\">\n                      <div class=\"flex items-center gap-2\">\n                        <span class=\"font-mono text-xs font-semibold tabular-nums\">{{ formatTime(item.start) }}</span>\n                        <span class=\"text-muted-foreground text-xs\">{{ item.speaker }}</span>\n                        @if (isActiveTranscript(item)) {\n                          <span ui-badge variant=\"secondary\" class=\"text-xs\">Now playing</span>\n                        }\n                      </div>\n                      <p class=\"mt-1 text-xs leading-relaxed sm:text-sm\">{{ item.text }}</p>\n                    </button>\n                  }\n                  @if (filteredTranscript().length === 0) {\n                    <p class=\"text-muted-foreground py-8 text-center text-xs\">\n                      No transcript lines match “{{ transcriptSearch() }}”.\n                    </p>\n                  }\n                </div>\n              </div>\n\n              <div ui-tabs-content value=\"notes\" class=\"flex min-h-0 flex-1 flex-col gap-3 p-4 outline-none\">\n                <div class=\"flex items-center gap-2\">\n                  <span ui-badge variant=\"outline\" class=\"gap-1 font-mono text-xs tabular-nums\">\n                    <lucide-icon [img]=\"Clock\" class=\"size-3\" />\n                    {{ formattedCurrentTime() }}\n                  </span>\n                  <span class=\"text-muted-foreground text-xs\">Note attaches at the current timestamp.</span>\n                </div>\n                <ui-textarea\n                  [value]=\"newNoteText()\"\n                  (valueChange)=\"newNoteText.set($event)\"\n                  [placeholder]=\"'Write your thoughts or takeaway at ' + formattedCurrentTime() + '...'\"\n                  [rows]=\"2\"\n                  class=\"resize-y text-xs leading-relaxed\"\n                  (keyDown)=\"handleNoteKeyDown($event)\"\n                />\n                <button\n                  ui-button\n                  size=\"sm\"\n                  class=\"gap-1.5 text-xs\"\n                  [disabled]=\"!newNoteText().trim()\"\n                  (click)=\"handleAddNote()\"\n                >\n                  <lucide-icon [img]=\"Plus\" class=\"size-3.5\" />\n                  <span>Add Note at {{ formattedCurrentTime() }}</span>\n                </button>\n                <div class=\"min-h-0 flex-1 space-y-2 overflow-auto\">\n                  @for (note of notesList(); track note.id) {\n                    <div class=\"border-border bg-muted/30 rounded-lg border p-3\">\n                      <div class=\"flex items-center justify-between gap-2\">\n                        <button\n                          type=\"button\"\n                          class=\"text-primary font-mono text-xs font-semibold tabular-nums hover:underline\"\n                          (click)=\"seekTo(note.timestamp)\"\n                        >\n                          {{ formatTime(note.timestamp) }}\n                        </button>\n                        <div class=\"flex items-center gap-1\">\n                          <span class=\"text-muted-foreground text-xs\">{{ note.createdAt }}</span>\n                          <button\n                            ui-button\n                            variant=\"ghost\"\n                            size=\"icon-sm\"\n                            class=\"size-6\"\n                            [attr.aria-label]=\"'Delete note ' + note.id\"\n                            (click)=\"handleDeleteNote(note.id)\"\n                          >\n                            <lucide-icon [img]=\"Trash2\" class=\"size-3.5\" />\n                          </button>\n                        </div>\n                      </div>\n                      <p class=\"text-foreground mt-1 text-xs leading-relaxed sm:text-sm\">{{ note.text }}</p>\n                    </div>\n                  }\n                </div>\n              </div>\n\n              <div ui-tabs-content value=\"qa\" class=\"flex min-h-0 flex-1 flex-col gap-3 p-4 outline-none\">\n                <ui-textarea\n                  [value]=\"newQuestionText()\"\n                  (valueChange)=\"newQuestionText.set($event)\"\n                  placeholder=\"Ask a question about this topic...\"\n                  [rows]=\"2\"\n                  class=\"text-xs\"\n                  (keyDown)=\"handleQuestionKeyDown($event)\"\n                />\n                <button\n                  ui-button\n                  size=\"sm\"\n                  class=\"gap-1.5 text-xs\"\n                  [disabled]=\"!newQuestionText().trim()\"\n                  (click)=\"handleAddQuestion()\"\n                >\n                  <lucide-icon [img]=\"Plus\" class=\"size-3.5\" />\n                  <span>Post Question</span>\n                </button>\n                <div class=\"min-h-0 flex-1 space-y-3 overflow-auto\">\n                  @for (q of questionsList(); track q.id) {\n                    <div class=\"border-border rounded-lg border p-3\">\n                      <div class=\"flex items-start justify-between gap-2\">\n                        <div class=\"flex items-center gap-2\">\n                          <div\n                            class=\"bg-primary/10 text-primary flex size-7 items-center justify-center rounded-full text-xs font-bold\"\n                          >\n                            {{ q.avatar }}\n                          </div>\n                          <div>\n                            <p class=\"text-foreground text-xs font-semibold\">{{ q.author }}</p>\n                            <p class=\"text-muted-foreground font-mono text-xs tabular-nums\">\n                              {{ formatTime(q.timestamp) }}\n                            </p>\n                          </div>\n                        </div>\n                        <button\n                          ui-button\n                          variant=\"outline\"\n                          size=\"sm\"\n                          class=\"h-7 gap-1 px-2 text-xs tabular-nums\"\n                          [attr.aria-label]=\"'Upvote question ' + q.id\"\n                          [class]=\"q.hasUpvoted ? 'border-primary text-primary' : ''\"\n                          (click)=\"toggleUpvote(q)\"\n                        >\n                          <lucide-icon [img]=\"ThumbsUp\" class=\"size-3\" />\n                          <span>{{ q.upvotes }}</span>\n                        </button>\n                      </div>\n                      <p class=\"text-foreground mt-2 text-xs leading-relaxed sm:text-sm\">{{ q.question }}</p>\n                      @if (q.answer) {\n                        <div class=\"bg-muted/40 border-border mt-2 rounded-md border p-2.5\">\n                          <p class=\"flex items-center gap-1.5 text-xs font-semibold\">\n                            <span>{{ q.answer.author }}</span>\n                            <span ui-badge variant=\"secondary\" class=\"text-xs\">{{ q.answer.role }}</span>\n                          </p>\n                          <p class=\"text-muted-foreground mt-1 text-xs leading-relaxed\">{{ q.answer.text }}</p>\n                        </div>\n                      }\n                    </div>\n                  }\n                </div>\n              </div>\n            </ui-tabs>\n          </div>\n        </aside>\n      </div>\n    </div>\n  `,\n})\nexport class UiLessonVideoPlayerComponent implements OnDestroy {\n  protected readonly ArrowRight = ArrowRight\n  protected readonly Bookmark = Bookmark\n  protected readonly BookmarkCheck = BookmarkCheck\n  protected readonly Check = Check\n  protected readonly CheckCircle2 = CheckCircle2\n  protected readonly ChevronRight = ChevronRight\n  protected readonly Clock = Clock\n  protected readonly Download = Download\n  protected readonly FileText = FileText\n  protected readonly Layers = Layers\n  protected readonly Lightbulb = Lightbulb\n  protected readonly Maximize2 = Maximize2\n  protected readonly MessageSquare = MessageSquare\n  protected readonly Pause = Pause\n  protected readonly Play = Play\n  protected readonly Plus = Plus\n  protected readonly RotateCcw = RotateCcw\n  protected readonly RotateCw = RotateCw\n  protected readonly Search = Search\n  protected readonly Subtitles = Subtitles\n  protected readonly ThumbsUp = ThumbsUp\n  protected readonly Trash2 = Trash2\n  protected readonly Volume2 = Volume2\n  protected readonly VolumeX = VolumeX\n\n  @Input('class') className?: string\n\n  readonly isPlaying = signal(false)\n  readonly currentTime = signal(384)\n  readonly totalDuration = signal(1120)\n  readonly bufferedPercent = signal(78)\n  readonly volume = signal(85)\n  readonly isMuted = signal(false)\n  readonly playbackSpeed = signal(1.25)\n  readonly speedOptions = [0.75, 1.0, 1.25, 1.5, 1.75, 2.0]\n  readonly showCaptions = signal(true)\n  readonly isFullscreen = signal(false)\n  readonly activeTab = signal<'transcript' | 'notes' | 'qa'>('transcript')\n  readonly transcriptSearch = signal('')\n  readonly isCompleted = signal(false)\n  readonly isBookmarked = signal(false)\n  readonly copiedAsset = signal<string | null>(null)\n\n  readonly transcriptItems = signal<TranscriptItem[]>(transcriptSeed.map((t) => ({ ...t })))\n  readonly newNoteText = signal('')\n  readonly notesList = signal<NoteItem[]>(notesSeed.map((n) => ({ ...n })))\n  readonly newQuestionText = signal('')\n  readonly questionsList = signal<QuestionItem[]>(\n    questionsSeed.map((q) => ({ ...q, answer: q.answer ? { ...q.answer } : undefined })),\n  )\n\n  private playInterval: ReturnType<typeof setInterval> | null = null\n  private copyTimer: ReturnType<typeof setTimeout> | undefined\n\n  readonly formattedCurrentTime = computed(() => formatTime(this.currentTime()))\n  readonly formattedTotalTime = computed(() => formatTime(this.totalDuration()))\n  readonly progressPercent = computed(() => (this.currentTime() / this.totalDuration()) * 100)\n\n  readonly activeTranscriptItem = computed(\n    () =>\n      this.transcriptItems().find((item) => this.currentTime() >= item.start && this.currentTime() < item.end) ??\n      this.transcriptItems()[0],\n  )\n\n  readonly filteredTranscript = computed(() => {\n    const query = this.transcriptSearch().trim().toLowerCase()\n    if (!query) return this.transcriptItems()\n    return this.transcriptItems().filter(\n      (item) => item.text.toLowerCase().includes(query) || item.speaker.toLowerCase().includes(query),\n    )\n  })\n\n  get rootClass(): string {\n    return cn('bg-background text-foreground w-full space-y-5', this.className)\n  }\n\n  formatTime(seconds: number): string {\n    return formatTime(seconds)\n  }\n\n  togglePlay(): void {\n    this.isPlaying.update((v) => !v)\n    if (this.isPlaying()) {\n      this.playInterval = setInterval(() => {\n        if (this.currentTime() < this.totalDuration()) {\n          this.currentTime.update((t) => t + 1)\n        } else {\n          this.isPlaying.set(false)\n          if (this.playInterval) clearInterval(this.playInterval)\n        }\n      }, 1000 / this.playbackSpeed())\n    } else if (this.playInterval) {\n      clearInterval(this.playInterval)\n    }\n  }\n\n  seekTo(seconds: number): void {\n    this.currentTime.set(Math.max(0, Math.min(seconds, this.totalDuration())))\n  }\n\n  handleSeekClick(event: MouseEvent): void {\n    const target = event.currentTarget as HTMLElement\n    const rect = target.getBoundingClientRect()\n    const ratio = Math.max(0, Math.min(1, (event.clientX - rect.left) / rect.width))\n    this.seekTo(Math.round(ratio * this.totalDuration()))\n  }\n\n  skipSeconds(delta: number): void {\n    this.seekTo(this.currentTime() + delta)\n  }\n\n  cycleSpeed(): void {\n    const idx = this.speedOptions.indexOf(this.playbackSpeed())\n    this.playbackSpeed.set(this.speedOptions[(idx + 1) % this.speedOptions.length]!)\n    if (this.isPlaying()) {\n      if (this.playInterval) clearInterval(this.playInterval)\n      this.playInterval = setInterval(() => {\n        if (this.currentTime() < this.totalDuration()) {\n          this.currentTime.update((t) => t + 1)\n        } else {\n          this.isPlaying.set(false)\n          if (this.playInterval) clearInterval(this.playInterval)\n        }\n      }, 1000 / this.playbackSpeed())\n    }\n  }\n\n  toggleMute(): void {\n    this.isMuted.update((v) => !v)\n  }\n\n  isActiveTranscript(item: TranscriptItem): boolean {\n    return this.activeTranscriptItem()?.id === item.id\n  }\n\n  transcriptRowClass(item: TranscriptItem): string {\n    return cn(\n      'w-full rounded-lg border p-3 text-left transition-colors',\n      this.isActiveTranscript(item)\n        ? 'border-primary/40 bg-primary/5'\n        : 'border-transparent hover:bg-muted/40 hover:border-border',\n    )\n  }\n\n  handleAddNote(): void {\n    const text = this.newNoteText().trim()\n    if (!text) return\n    this.notesList.update((prev) => [\n      { id: `note-${Date.now()}`, timestamp: this.currentTime(), text, createdAt: 'Just now' },\n      ...prev,\n    ])\n    this.newNoteText.set('')\n  }\n\n  handleDeleteNote(id: string): void {\n    this.notesList.update((prev) => prev.filter((n) => n.id !== id))\n  }\n\n  handleNoteKeyDown(event: KeyboardEvent): void {\n    if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n      event.preventDefault()\n      this.handleAddNote()\n    }\n  }\n\n  handleQuestionKeyDown(event: KeyboardEvent): void {\n    if (event.key === 'Enter' && !event.shiftKey && !event.metaKey && !event.ctrlKey) {\n      event.preventDefault()\n      this.handleAddQuestion()\n    }\n  }\n\n  handleAddQuestion(): void {\n    const text = this.newQuestionText().trim()\n    if (!text) return\n    this.questionsList.update((prev) => [\n      {\n        id: `q-${Date.now()}`,\n        timestamp: this.currentTime(),\n        author: 'You (Student)',\n        avatar: 'ME',\n        question: text,\n        upvotes: 1,\n        hasUpvoted: true,\n      },\n      ...prev,\n    ])\n    this.newQuestionText.set('')\n  }\n\n  toggleUpvote(q: QuestionItem): void {\n    this.questionsList.update((prev) =>\n      prev.map((item) =>\n        item.id === q.id\n          ? { ...item, upvotes: item.upvotes + (item.hasUpvoted ? -1 : 1), hasUpvoted: !item.hasUpvoted }\n          : item,\n      ),\n    )\n  }\n\n  handleCopyAsset(name: string): void {\n    this.copiedAsset.set(name)\n    clearTimeout(this.copyTimer)\n    this.copyTimer = setTimeout(() => this.copiedAsset.set(null), 2000)\n  }\n\n  ngOnDestroy(): void {\n    if (this.playInterval) clearInterval(this.playInterval)\n    clearTimeout(this.copyTimer)\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/lesson-video-player/lesson-video-player.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/lesson-video-player/index.ts",
      "content": "export {\n  UiLessonVideoPlayerComponent,\n  transcriptSeed,\n  notesSeed,\n  questionsSeed,\n  formatTime,\n  type TranscriptItem,\n  type NoteItem,\n  type QuestionItem,\n} from './lesson-video-player.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/lesson-video-player/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/input.json",
    "https://uipkge.dev/r/angular/progress.json",
    "https://uipkge.dev/r/angular/tabs.json",
    "https://uipkge.dev/r/angular/textarea.json"
  ],
  "description": "Online lecture video player with playback speed selector, interactive timestamped transcript, and note-taking drawer.",
  "categories": [
    "education",
    "app",
    "media"
  ]
}