{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "customer-reviews",
  "title": "Customer Reviews",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/customer-reviews/customer-reviews.component.ts",
      "content": "// Product page \"Customer Reviews\" section. Edit the sample data below -- state is local, so wire\n// `toggleHelpful()`, `loadMore()` and `submitReview()` to your own reviews API.\nimport { Component, computed, signal, ChangeDetectionStrategy } from '@angular/core'\nimport { CheckCircle2, Image as ImageIcon, LucideAngularModule, PenLine, Star, ThumbsUp, X } from 'lucide-angular'\nimport { cn } from '@/lib/utils'\nimport { UiAvatarComponent, UiAvatarFallbackComponent, UiAvatarImageComponent } from '@/ui/avatar/avatar.component'\nimport { UiBadgeComponent } from '@/ui/badge/badge.component'\nimport { UiButtonComponent } from '@/ui/button/button.component'\nimport { UiCardComponent } from '@/ui/card/card.component'\nimport { UiProgressComponent } from '@/ui/progress/progress.component'\nimport { UiRatingComponent } from '@/ui/rating/rating.component'\nimport {\n  UiSelectComponent,\n  UiSelectContentComponent,\n  UiSelectItemComponent,\n  UiSelectTriggerComponent,\n  UiSelectValueComponent,\n} from '@/ui/select/select.component'\n\nexport interface ReviewItem {\n  id: string\n  author: string\n  initials: string\n  avatar?: string\n  rating: number\n  date: string\n  title: string\n  body: string\n  variant: string\n  verified: boolean\n  helpfulCount: number\n  isHelpful?: boolean\n  photos?: string[]\n}\n\nexport type ReviewSort = 'recent' | 'highest' | 'lowest' | 'helpful'\n\ninterface RatingBar {\n  stars: number\n  percentage: number\n  count: number\n}\n\ninterface PhotoGalleryItem {\n  id: string\n  url: string\n  alt: string\n  author: string\n}\n\nconst ratingBreakdowns: RatingBar[] = [\n  { stars: 5, percentage: 78, count: 267 },\n  { stars: 4, percentage: 14, count: 48 },\n  { stars: 3, percentage: 5, count: 17 },\n  { stars: 2, percentage: 2, count: 7 },\n  { stars: 1, percentage: 1, count: 3 },\n]\n\nconst customerPhotos: PhotoGalleryItem[] = [\n  {\n    id: 'p1',\n    url: 'https://images.unsplash.com/photo-1546435770-a3e426bf472b?w=400&auto=format&fit=crop&q=80',\n    alt: 'Customer photo of matte navy headphones on wood desk',\n    author: 'Marcus V.',\n  },\n  {\n    id: 'p2',\n    url: 'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=400&auto=format&fit=crop&q=80',\n    alt: 'Customer photo of headphones studio unboxing',\n    author: 'Elena R.',\n  },\n  {\n    id: 'p3',\n    url: 'https://images.unsplash.com/photo-1484704849700-f032a568e944?w=400&auto=format&fit=crop&q=80',\n    alt: 'Customer photo of cushioned earpads close up',\n    author: 'Devon C.',\n  },\n  {\n    id: 'p4',\n    url: 'https://images.unsplash.com/photo-1583394838336-acd977736f90?w=400&auto=format&fit=crop&q=80',\n    alt: 'Customer photo wearing headphones in coffee shop',\n    author: 'Amara D.',\n  },\n  {\n    id: 'p5',\n    url: 'https://images.unsplash.com/photo-1590658268037-6bf12165a8df?w=400&auto=format&fit=crop&q=80',\n    alt: 'Customer photo of travel case and cable accessories',\n    author: 'Liam G.',\n  },\n  {\n    id: 'p6',\n    url: 'https://images.unsplash.com/photo-1545127398-14699f92334b?w=400&auto=format&fit=crop&q=80',\n    alt: 'Customer photo wearing silver headphones while working',\n    author: 'Sophia M.',\n  },\n]\n\nconst initialReviews: ReviewItem[] = [\n  {\n    id: 'rev-1',\n    author: 'Marcus Vance',\n    initials: 'MV',\n    avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=160&auto=format&fit=crop&q=80',\n    rating: 5,\n    date: '2 days ago',\n    title: 'Exceeded all my expectations — best studio acoustic fidelity',\n    body: 'The soundstage is remarkably wide and bass response is tight without overpowering the mids. Build quality is aerospace-grade, and the memory foam ear cushions remain comfortable even through 8-hour mixing sessions. Multi-device Bluetooth pairing switches directly between MacBook and phone.',\n    variant: 'Color: Matte Navy · Size: L',\n    verified: true,\n    helpfulCount: 24,\n    isHelpful: false,\n    photos: [\n      'https://images.unsplash.com/photo-1546435770-a3e426bf472b?w=400&auto=format&fit=crop&q=80',\n      'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=400&auto=format&fit=crop&q=80',\n    ],\n  },\n  {\n    id: 'rev-2',\n    author: 'Elena Rostova',\n    initials: 'ER',\n    avatar: 'https://images.unsplash.com/photo-1517841905240-472988babdf9?w=160&auto=format&fit=crop&q=80',\n    rating: 5,\n    date: '1 week ago',\n    title: 'ANC handles subway roar with zero pressure sensation',\n    body: 'Active noise cancellation on these is sublime. Unlike other flagships that create an uncomfortable eardrum suction pressure, this feels completely natural. Battery life easily stretches across 4 days of heavy commuting without a recharge.',\n    variant: 'Color: Silver Frost · Size: M',\n    verified: true,\n    helpfulCount: 19,\n    isHelpful: false,\n    photos: ['https://images.unsplash.com/photo-1583394838336-acd977736f90?w=400&auto=format&fit=crop&q=80'],\n  },\n  {\n    id: 'rev-3',\n    author: 'Devon Chen',\n    initials: 'DC',\n    avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=160&auto=format&fit=crop&q=80',\n    rating: 4,\n    date: '2 weeks ago',\n    title: 'Fantastic sound and ergonomics, case could be slightly slimmer',\n    body: 'Everything about audio reproduction is 10/10. The companion app EQ presets are actually tuned well instead of gimmicky. Only minor critique is the hard-shell travel case is a bit bulky for slim laptop bags, but the protection is undisputed.',\n    variant: 'Color: Matte Navy · Size: L',\n    verified: true,\n    helpfulCount: 12,\n    isHelpful: false,\n    photos: ['https://images.unsplash.com/photo-1484704849700-f032a568e944?w=400&auto=format&fit=crop&q=80'],\n  },\n  {\n    id: 'rev-4',\n    author: 'Amara Diallo',\n    initials: 'AD',\n    avatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=160&auto=format&fit=crop&q=80',\n    rating: 5,\n    date: '3 weeks ago',\n    title: 'Crystal-clear microphone quality for all-day client calls',\n    body: 'I purchased these primarily for conference calls in a busy open-plan studio. The beamforming microphones filter out nearby background conversations so callers only hear my voice. Lightweight headband design does not induce fatigue.',\n    variant: 'Color: Space Black · Size: L',\n    verified: true,\n    helpfulCount: 8,\n    isHelpful: false,\n  },\n]\n\nconst extraReviews: ReviewItem[] = [\n  {\n    id: 'rev-5',\n    author: 'Liam Gallagher',\n    initials: 'LG',\n    avatar: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=160&auto=format&fit=crop&q=80',\n    rating: 5,\n    date: '1 month ago',\n    title: 'Premium finish that justifies every single penny',\n    body: 'The anodized aluminum arms and tactile physical dials feel so much better than finicky touch controls in cold weather. Fast USB-C charging gave me 6 hours of playback from a 15-minute coffee break charge.',\n    variant: 'Color: Space Black · Size: L',\n    verified: true,\n    helpfulCount: 15,\n    isHelpful: false,\n    photos: ['https://images.unsplash.com/photo-1590658268037-6bf12165a8df?w=400&auto=format&fit=crop&q=80'],\n  },\n  {\n    id: 'rev-6',\n    author: 'Sophia Martinez',\n    initials: 'SM',\n    avatar: 'https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=160&auto=format&fit=crop&q=80',\n    rating: 4,\n    date: '1 month ago',\n    title: 'Great balanced frequency response for classical and jazz',\n    body: 'Instruments have pinpoint positional separation in spatial mode. The high frequencies are crisp without ever becoming sibilant or harsh at high volume.',\n    variant: 'Color: Silver Frost · Size: M',\n    verified: false,\n    helpfulCount: 6,\n    isHelpful: false,\n  },\n]\n\nconst sortValues: readonly ReviewSort[] = ['recent', 'highest', 'lowest', 'helpful']\n\n/**\n * CustomerReviews -- the React block, 1:1: a ratings overview card (4.8 score, clickable\n * distribution bars that filter by star count, customer photo grid), a toggleable \"Write Your\n * Review\" form, filter chips + sort select, verified-buyer review cards with attached photos and\n * a reactive Helpful counter, \"Load More Reviews\", and a photo lightbox.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-customer-reviews, [ui-customer-reviews]',\n  standalone: true,\n  // React renders the root <section> itself: the host stays out of layout.\n  host: { class: 'contents' },\n  imports: [\n    LucideAngularModule,\n    UiAvatarComponent,\n    UiAvatarImageComponent,\n    UiAvatarFallbackComponent,\n    UiBadgeComponent,\n    UiButtonComponent,\n    UiCardComponent,\n    UiProgressComponent,\n    UiRatingComponent,\n    UiSelectComponent,\n    UiSelectTriggerComponent,\n    UiSelectValueComponent,\n    UiSelectContentComponent,\n    UiSelectItemComponent,\n  ],\n  template: `\n    <section data-slot=\"customer-reviews\" class=\"w-full space-y-8\">\n      <!-- Section Header -->\n      <div class=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n        <div>\n          <h2 class=\"text-foreground text-2xl font-bold tracking-tight sm:text-3xl\">Customer Reviews</h2>\n          <p class=\"text-muted-foreground mt-1 text-sm\">Real feedback from verified purchasers</p>\n        </div>\n        <button\n          ui-button\n          aria-label=\"Close review form\"\n          type=\"button\"\n          class=\"h-9 gap-2 text-xs font-medium sm:text-sm\"\n          (click)=\"isWritingReview.set(!isWritingReview())\"\n        >\n          <lucide-icon [img]=\"PenLine\" class=\"size-4\" />\n          Write a Review\n        </button>\n      </div>\n\n      <!-- Ratings Overview Card (3-column layout on md+) -->\n      <div ui-card class=\"border-border bg-card p-6 shadow-xs\">\n        <div class=\"grid grid-cols-1 gap-6 md:grid-cols-12 md:items-center lg:gap-8\">\n          <!-- Col 1: Big Rating Display -->\n          <div\n            class=\"border-border flex flex-col items-center justify-center space-y-2 border-b pb-6 text-center md:col-span-4 md:items-start md:border-r md:border-b-0 md:pr-6 md:pb-0 md:text-left lg:col-span-3\"\n          >\n            <div class=\"flex items-baseline gap-2\">\n              <span class=\"text-foreground text-4xl font-bold tracking-tight\">4.8</span>\n              <span class=\"text-muted-foreground text-sm font-medium\">out of 5</span>\n            </div>\n            <ui-rating [value]=\"4.8\" readonly halfIncrements size=\"medium\" />\n            <p class=\"text-muted-foreground text-xs\">Based on 342 reviews</p>\n            <span\n              ui-badge\n              variant=\"secondary\"\n              class=\"border-success/20 bg-success/10 text-success mt-1 gap-1.5 font-medium\"\n            >\n              <lucide-icon [img]=\"CheckCircle2\" class=\"size-3.5\" />\n              96% recommend\n            </span>\n          </div>\n\n          <!-- Col 2: Rating Distribution Bars -->\n          <div\n            class=\"border-border space-y-2 border-b pb-6 md:col-span-5 md:border-r md:border-b-0 md:pr-6 md:pb-0 lg:col-span-5\"\n          >\n            @for (bar of ratingBreakdowns; track bar.stars) {\n              <button type=\"button\" [class]=\"barClass(bar.stars)\" (click)=\"toggleRatingFilter(bar.stars)\">\n                <span class=\"text-foreground flex w-12 items-center gap-1 font-medium\">\n                  {{ bar.stars }} <lucide-icon [img]=\"Star\" class=\"fill-warning text-warning size-3\" />\n                </span>\n                <ui-progress [value]=\"bar.percentage\" class=\"h-2 flex-1\" />\n                <span class=\"text-muted-foreground group-hover:text-foreground w-10 text-right font-mono\">\n                  {{ bar.percentage }}%\n                </span>\n              </button>\n            }\n          </div>\n\n          <!-- Col 3: Customer Photos / Videos Gallery -->\n          <div class=\"space-y-3 md:col-span-3 lg:col-span-4\">\n            <div class=\"flex items-center justify-between\">\n              <h4 class=\"text-foreground text-sm font-semibold\">Customer Photos (48)</h4>\n              <span class=\"text-muted-foreground text-xs\">Recent</span>\n            </div>\n            <div class=\"grid grid-cols-3 gap-2\">\n              @for (photo of customerPhotos; track photo.id; let index = $index) {\n                <button\n                  type=\"button\"\n                  class=\"group border-border bg-muted focus-visible:ring-ring relative aspect-square cursor-pointer overflow-hidden rounded-lg border focus-visible:ring-2 focus-visible:outline-none\"\n                  [attr.aria-label]=\"photo.alt\"\n                  (click)=\"activePhotoUrl.set(photo.url)\"\n                >\n                  <img\n                    [src]=\"photo.url\"\n                    [alt]=\"photo.alt\"\n                    class=\"size-full object-cover transition-transform duration-300 group-hover:scale-105\"\n                  />\n                  @if (index === 5) {\n                    <div\n                      class=\"absolute inset-0 flex items-center justify-center bg-black/60 text-xs font-semibold text-white transition-colors group-hover:bg-black/70\"\n                    >\n                      +43 more\n                    </div>\n                  }\n                </button>\n              }\n            </div>\n          </div>\n        </div>\n      </div>\n\n      <!-- Write a Review Expandable Panel -->\n      @if (isWritingReview()) {\n        <div ui-card class=\"border-primary/30 bg-card p-6 shadow-sm\">\n          <div class=\"space-y-4\">\n            <div class=\"flex items-center justify-between\">\n              <h3 class=\"text-foreground text-base font-semibold\">Write Your Review</h3>\n              <button\n                ui-button\n                aria-label=\"Close review form\"\n                type=\"button\"\n                variant=\"ghost\"\n                size=\"icon\"\n                class=\"text-muted-foreground hover:text-foreground size-8\"\n                (click)=\"isWritingReview.set(false)\"\n              >\n                <lucide-icon [img]=\"X\" class=\"size-4\" />\n              </button>\n            </div>\n\n            <div class=\"space-y-3\">\n              <div>\n                <label class=\"text-foreground mb-1.5 block text-xs font-medium\">Overall Rating</label>\n                <ui-rating [value]=\"newRating()\" (valueChange)=\"newRating.set($event)\" size=\"medium\" hover />\n              </div>\n\n              <div class=\"grid gap-3 sm:grid-cols-2\">\n                <div>\n                  <label class=\"text-foreground mb-1.5 block text-xs font-medium\">Your Name</label>\n                  <input\n                    [value]=\"newAuthor()\"\n                    (input)=\"newAuthor.set(inputValue($event))\"\n                    type=\"text\"\n                    placeholder=\"e.g. Alex Morgan\"\n                    class=\"border-border bg-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 w-full rounded-md border px-3 py-1 text-xs shadow-xs transition-colors focus-visible:ring-2 focus-visible:outline-none\"\n                  />\n                </div>\n                <div>\n                  <label class=\"text-foreground mb-1.5 block text-xs font-medium\">Purchased Option</label>\n                  <input\n                    [value]=\"newVariant()\"\n                    (input)=\"newVariant.set(inputValue($event))\"\n                    type=\"text\"\n                    placeholder=\"e.g. Color: Space Black · Size: L\"\n                    class=\"border-border bg-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 w-full rounded-md border px-3 py-1 text-xs shadow-xs transition-colors focus-visible:ring-2 focus-visible:outline-none\"\n                  />\n                </div>\n              </div>\n\n              <div>\n                <label class=\"text-foreground mb-1.5 block text-xs font-medium\">Review Headline</label>\n                <input\n                  [value]=\"newTitle()\"\n                  (input)=\"newTitle.set(inputValue($event))\"\n                  type=\"text\"\n                  placeholder=\"Summarize your experience in one sentence\"\n                  class=\"border-border bg-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 w-full rounded-md border px-3 py-1 text-xs shadow-xs transition-colors focus-visible:ring-2 focus-visible:outline-none\"\n                />\n              </div>\n\n              <div>\n                <label class=\"text-foreground mb-1.5 block text-xs font-medium\">Detailed Feedback</label>\n                <textarea\n                  [value]=\"newBody()\"\n                  (input)=\"newBody.set(inputValue($event))\"\n                  rows=\"3\"\n                  placeholder=\"What did you like or dislike? How does it perform?\"\n                  class=\"border-border bg-background placeholder:text-muted-foreground focus-visible:ring-ring flex w-full rounded-md border px-3 py-2 text-xs shadow-xs transition-colors focus-visible:ring-2 focus-visible:outline-none\"\n                ></textarea>\n              </div>\n\n              <div class=\"flex items-center justify-end gap-2 pt-2\">\n                <button\n                  ui-button\n                  aria-label=\"Close review form\"\n                  type=\"button\"\n                  variant=\"outline\"\n                  size=\"sm\"\n                  class=\"h-8 text-xs\"\n                  (click)=\"isWritingReview.set(false)\"\n                >\n                  Cancel\n                </button>\n                <button\n                  ui-button\n                  type=\"button\"\n                  size=\"sm\"\n                  class=\"h-8 text-xs\"\n                  [disabled]=\"!canSubmit()\"\n                  (click)=\"submitReview()\"\n                >\n                  Submit Review\n                </button>\n              </div>\n            </div>\n          </div>\n        </div>\n      }\n\n      <!-- Review Filters & Sort Toolbar -->\n      <div class=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n        <!-- Filter chips -->\n        <div class=\"flex flex-wrap items-center gap-2\">\n          <button\n            ui-button\n            type=\"button\"\n            size=\"sm\"\n            [variant]=\"noFilters() ? 'default' : 'outline'\"\n            class=\"h-8 rounded-full text-xs\"\n            (click)=\"resetFilters()\"\n          >\n            All Ratings\n          </button>\n          <button\n            ui-button\n            type=\"button\"\n            size=\"sm\"\n            [variant]=\"selectedRating() === 5 ? 'default' : 'outline'\"\n            class=\"h-8 gap-1 rounded-full text-xs\"\n            (click)=\"toggleRatingFilter(5)\"\n          >\n            @if (selectedRating() === 5) {\n              <lucide-icon [img]=\"Star\" class=\"size-3 fill-current\" />\n            } @else {\n              <lucide-icon [img]=\"Star\" class=\"fill-warning text-warning size-3\" />\n            }\n            5 Stars\n          </button>\n          <button\n            ui-button\n            type=\"button\"\n            size=\"sm\"\n            [variant]=\"withPhotosOnly() ? 'default' : 'outline'\"\n            class=\"h-8 gap-1.5 rounded-full text-xs\"\n            (click)=\"withPhotosOnly.set(!withPhotosOnly())\"\n          >\n            <lucide-icon [img]=\"ImageIcon\" class=\"size-3.5\" />\n            With Photos\n          </button>\n          <button\n            ui-button\n            type=\"button\"\n            size=\"sm\"\n            [variant]=\"verifiedOnly() ? 'default' : 'outline'\"\n            class=\"h-8 gap-1.5 rounded-full text-xs\"\n            (click)=\"verifiedOnly.set(!verifiedOnly())\"\n          >\n            <lucide-icon [img]=\"CheckCircle2\" class=\"size-3.5\" />\n            Verified Only\n          </button>\n        </div>\n\n        <!-- Sort Dropdown -->\n        <div class=\"flex items-center gap-2\">\n          <span class=\"text-muted-foreground text-xs font-medium\">Sort by:</span>\n          <ui-select [value]=\"sortBy()\" (valueChange)=\"setSort($event)\">\n            <button ui-select-trigger class=\"h-8 w-36 text-xs sm:w-40\">\n              <ui-select-value placeholder=\"Sort by\" />\n            </button>\n            <ui-select-content>\n              <ui-select-item value=\"recent\">Most Recent</ui-select-item>\n              <ui-select-item value=\"highest\">Highest Rated</ui-select-item>\n              <ui-select-item value=\"lowest\">Lowest Rated</ui-select-item>\n              <ui-select-item value=\"helpful\">Most Helpful</ui-select-item>\n            </ui-select-content>\n          </ui-select>\n        </div>\n      </div>\n\n      <!-- Reviews List -->\n      <div class=\"space-y-4\">\n        @for (rev of filteredReviews(); track rev.id) {\n          <div ui-card class=\"border-border bg-card p-5 shadow-xs\">\n            <div class=\"space-y-4\">\n              <!-- Header Row -->\n              <div class=\"flex flex-wrap items-start justify-between gap-3\">\n                <div class=\"flex items-center gap-3\">\n                  <ui-avatar class=\"border-border size-10 border\">\n                    @if (rev.avatar) {\n                      <ui-avatar-image [src]=\"rev.avatar\" [alt]=\"rev.author\" />\n                    }\n                    <ui-avatar-fallback class=\"text-xs font-medium\">{{ rev.initials }}</ui-avatar-fallback>\n                  </ui-avatar>\n                  <div class=\"space-y-0.5\">\n                    <div class=\"flex items-center gap-2\">\n                      <span class=\"text-foreground text-sm font-semibold\">{{ rev.author }}</span>\n                      @if (rev.verified) {\n                        <span\n                          ui-badge\n                          variant=\"secondary\"\n                          class=\"text-muted-foreground h-5 gap-1 px-1.5 text-xs font-normal\"\n                        >\n                          <lucide-icon [img]=\"CheckCircle2\" class=\"text-success size-3\" />\n                          Verified Buyer\n                        </span>\n                      }\n                    </div>\n                    <time class=\"text-muted-foreground text-xs\">{{ rev.date }}</time>\n                  </div>\n                </div>\n                <ui-rating [value]=\"rev.rating\" readonly size=\"small\" />\n              </div>\n\n              <!-- Review Content -->\n              <div class=\"space-y-2\">\n                <h5 class=\"text-foreground text-sm font-semibold sm:text-base\">{{ rev.title }}</h5>\n                <p class=\"text-muted-foreground text-xs leading-relaxed sm:text-sm\">{{ rev.body }}</p>\n              </div>\n\n              <!-- Product Variant -->\n              @if (rev.variant) {\n                <div\n                  class=\"bg-muted/60 text-muted-foreground inline-flex items-center gap-1.5 rounded-md px-2.5 py-1 text-xs font-medium\"\n                >\n                  <span>{{ rev.variant }}</span>\n                </div>\n              }\n\n              <!-- Attached Photos -->\n              @if (rev.photos && rev.photos.length > 0) {\n                <div class=\"flex flex-wrap gap-2 pt-1\">\n                  @for (photoUrl of rev.photos; track $index; let pIdx = $index) {\n                    <button\n                      type=\"button\"\n                      class=\"group border-border bg-muted focus-visible:ring-ring relative size-16 cursor-pointer overflow-hidden rounded-lg border focus-visible:ring-2 focus-visible:outline-none sm:size-20\"\n                      [attr.aria-label]=\"'Review photo ' + (pIdx + 1) + ' by ' + rev.author\"\n                      (click)=\"activePhotoUrl.set(photoUrl)\"\n                    >\n                      <img\n                        [src]=\"photoUrl\"\n                        [alt]=\"'Review photo ' + (pIdx + 1) + ' by ' + rev.author\"\n                        class=\"size-full object-cover transition-transform duration-300 group-hover:scale-105\"\n                      />\n                    </button>\n                  }\n                </div>\n              }\n\n              <!-- Helpful Action Button -->\n              <div class=\"border-border flex items-center justify-between border-t pt-3\">\n                <p class=\"text-muted-foreground text-xs\">Was this review helpful?</p>\n                <button\n                  ui-button\n                  type=\"button\"\n                  variant=\"outline\"\n                  size=\"sm\"\n                  [class]=\"helpfulClass(rev)\"\n                  (click)=\"toggleHelpful(rev.id)\"\n                >\n                  @if (rev.isHelpful) {\n                    <lucide-icon [img]=\"ThumbsUp\" class=\"fill-primary size-3.5\" />\n                  } @else {\n                    <lucide-icon [img]=\"ThumbsUp\" class=\"size-3.5\" />\n                  }\n                  Helpful ({{ rev.helpfulCount }})\n                </button>\n              </div>\n            </div>\n          </div>\n        } @empty {\n          <div class=\"border-border bg-card rounded-xl border p-12 text-center\">\n            <p class=\"text-foreground text-sm font-medium\">No reviews match the selected filters</p>\n            <p class=\"text-muted-foreground mt-1 text-xs\">Try clearing your filters to view all customer feedback</p>\n            <button\n              ui-button\n              type=\"button\"\n              variant=\"outline\"\n              size=\"sm\"\n              class=\"mt-4 h-8 text-xs\"\n              (click)=\"resetFilters()\"\n            >\n              Reset Filters\n            </button>\n          </div>\n        }\n      </div>\n\n      <!-- Pagination / Load More -->\n      <div class=\"border-border flex flex-col items-center justify-between gap-4 border-t pt-6 sm:flex-row\">\n        <p class=\"text-muted-foreground text-xs\">Showing 1-{{ filteredReviews().length }} of 342 reviews</p>\n        @if (!hasLoadedMore()) {\n          <button\n            ui-button\n            type=\"button\"\n            variant=\"outline\"\n            size=\"sm\"\n            class=\"h-9 px-4 text-xs font-medium\"\n            (click)=\"loadMore()\"\n          >\n            Load More Reviews\n          </button>\n        }\n      </div>\n\n      <!-- Photo Modal / Lightbox -->\n      @if (activePhotoUrl(); as photoUrl) {\n        <div\n          class=\"fixed inset-0 z-50 flex items-center justify-center bg-black/80 p-4 backdrop-blur-xs\"\n          (click)=\"activePhotoUrl.set(null)\"\n        >\n          <div\n            class=\"border-border bg-card relative max-h-[85vh] max-w-2xl overflow-hidden rounded-xl border p-3 shadow-sm\"\n            (click)=\"$event.stopPropagation()\"\n          >\n            <img\n              [src]=\"photoUrl\"\n              alt=\"Customer review photo preview\"\n              class=\"max-h-[75vh] w-auto rounded-lg object-contain\"\n            />\n            <div class=\"flex items-center justify-between p-2 pt-3\">\n              <span class=\"text-muted-foreground text-xs\">Customer Review Photo</span>\n              <button\n                ui-button\n                type=\"button\"\n                variant=\"ghost\"\n                size=\"sm\"\n                class=\"h-7 text-xs\"\n                (click)=\"activePhotoUrl.set(null)\"\n              >\n                Close\n              </button>\n            </div>\n          </div>\n        </div>\n      }\n    </section>\n  `,\n})\nexport class UiCustomerReviewsComponent {\n  protected readonly CheckCircle2 = CheckCircle2\n  protected readonly ImageIcon = ImageIcon\n  protected readonly PenLine = PenLine\n  protected readonly Star = Star\n  protected readonly ThumbsUp = ThumbsUp\n  protected readonly X = X\n\n  protected readonly ratingBreakdowns = ratingBreakdowns\n  protected readonly customerPhotos = customerPhotos\n\n  readonly reviews = signal<ReviewItem[]>(initialReviews)\n  readonly selectedRating = signal<number | null>(null)\n  readonly withPhotosOnly = signal(false)\n  readonly verifiedOnly = signal(false)\n  readonly sortBy = signal<ReviewSort>('recent')\n  readonly isWritingReview = signal(false)\n  readonly hasLoadedMore = signal(false)\n  readonly activePhotoUrl = signal<string | null>(null)\n\n  // Write review form states\n  readonly newRating = signal(5)\n  readonly newAuthor = signal('')\n  readonly newVariant = signal('Color: Matte Navy · Size: L')\n  readonly newTitle = signal('')\n  readonly newBody = signal('')\n\n  readonly noFilters = computed(() => this.selectedRating() === null && !this.withPhotosOnly() && !this.verifiedOnly())\n\n  readonly canSubmit = computed(() => !!this.newTitle().trim() && !!this.newBody().trim() && this.newRating() !== 0)\n\n  readonly filteredReviews = computed(() => {\n    let result = [...this.reviews()]\n    const rating = this.selectedRating()\n\n    if (rating !== null) {\n      result = result.filter((r) => r.rating === rating)\n    }\n\n    if (this.withPhotosOnly()) {\n      result = result.filter((r) => r.photos && r.photos.length > 0)\n    }\n\n    if (this.verifiedOnly()) {\n      result = result.filter((r) => r.verified)\n    }\n\n    const sortBy = this.sortBy()\n    if (sortBy === 'highest') {\n      result.sort((a, b) => b.rating - a.rating)\n    } else if (sortBy === 'lowest') {\n      result.sort((a, b) => a.rating - b.rating)\n    } else if (sortBy === 'helpful') {\n      result.sort((a, b) => b.helpfulCount - a.helpfulCount)\n    }\n\n    return result\n  })\n\n  protected barClass(stars: number): string {\n    return cn(\n      'group hover:bg-muted/60 focus-visible:ring-ring flex w-full items-center gap-3 rounded-md px-2 py-1 text-left text-xs transition-colors focus-visible:ring-2 focus-visible:outline-none',\n      this.selectedRating() === stars && 'bg-muted ring-primary/30 font-semibold ring-1',\n    )\n  }\n\n  protected helpfulClass(rev: ReviewItem): string {\n    return cn('h-8 gap-1.5 text-xs', rev.isHelpful && 'border-primary/40 bg-primary/10 text-primary font-medium')\n  }\n\n  protected inputValue(event: Event): string {\n    return (event.target as HTMLInputElement | HTMLTextAreaElement).value\n  }\n\n  setSort(value: string): void {\n    const next = sortValues.find((v) => v === value)\n    if (next) this.sortBy.set(next)\n  }\n\n  toggleRatingFilter(stars: number): void {\n    this.selectedRating.update((prev) => (prev === stars ? null : stars))\n  }\n\n  resetFilters(): void {\n    this.selectedRating.set(null)\n    this.withPhotosOnly.set(false)\n    this.verifiedOnly.set(false)\n  }\n\n  toggleHelpful(reviewId: string): void {\n    this.reviews.update((prev) =>\n      prev.map((rev) => {\n        if (rev.id === reviewId) {\n          const nextIsHelpful = !rev.isHelpful\n          return {\n            ...rev,\n            isHelpful: nextIsHelpful,\n            helpfulCount: nextIsHelpful ? rev.helpfulCount + 1 : rev.helpfulCount - 1,\n          }\n        }\n        return rev\n      }),\n    )\n  }\n\n  loadMore(): void {\n    this.reviews.update((prev) => [...prev, ...extraReviews])\n    this.hasLoadedMore.set(true)\n  }\n\n  submitReview(): void {\n    if (!this.canSubmit()) return\n    const authorName = this.newAuthor().trim() || 'Anonymous User'\n    const initials = authorName\n      .split(' ')\n      .map((n) => n[0])\n      .join('')\n      .toUpperCase()\n      .slice(0, 2)\n\n    const created: ReviewItem = {\n      id: `rev-${Date.now()}`,\n      author: authorName,\n      initials,\n      rating: this.newRating(),\n      date: 'Just now',\n      title: this.newTitle().trim(),\n      body: this.newBody().trim(),\n      variant: this.newVariant().trim() || 'Color: Matte Navy · Size: L',\n      verified: true,\n      helpfulCount: 0,\n      isHelpful: false,\n    }\n\n    this.reviews.update((prev) => [created, ...prev])\n    this.newTitle.set('')\n    this.newBody.set('')\n    this.newAuthor.set('')\n    this.newRating.set(5)\n    this.isWritingReview.set(false)\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/customer-reviews/customer-reviews.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/customer-reviews/index.ts",
      "content": "export { UiCustomerReviewsComponent, type ReviewItem, type ReviewSort } from './customer-reviews.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/customer-reviews/index.ts"
    }
  ],
  "dependencies": [
    "lucide-angular"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/avatar.json",
    "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/rating.json",
    "https://uipkge.dev/r/angular/select.json"
  ],
  "description": "Dedicated customer reviews section with ratings breakdown, interactive distribution bars, photo gallery with thumbnail overlay, filter chips, sort options, detailed verified buyer cards with attached photos, reactive helpful counter, and review submission form.",
  "categories": [
    "commerce",
    "ecommerce",
    "marketing"
  ]
}