Menu

Change language

GitHub repo

Logo Ticker Infinite

blockmarketing

Infinite marquee brand partner ticker with sector category filters, speed and pause controls, and interactive telemetry hover cards.

Installation

$npx uipkge-ng@latest add logo-ticker-infinite
Manifest:https://uipkge.dev/r/angular/logo-ticker-infinite.jsonInstalls to:src/app/components/blocks/logo-ticker-infinite/

Variants

Loading interactive previews…

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

TickerBrandPartner
interface TickerBrandPartner {
  id: string
  name: string
  category: 'devtools' | 'fintech' | 'enterprise'
  role: string
  metric: string
  region: string
}

npm dependencies

Includes

Files installed (2)

  • src/app/components/blocks/logo-ticker-infinite/logo-ticker-infinite.component.ts10.2 kB
    // Infinite marquee brand partner ticker with sector category filters, speed and
    // pause controls, and interactive telemetry hover cards. Port of the Vue/React
    // block 1:1 — the track repeats the filtered list twice for a seamless loop;
    // hover a tile to inspect its live integration telemetry.
    import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core'
    import { LucideAngularModule, Pause, Play } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    
    export interface TickerBrandPartner {
      id: string
      name: string
      category: 'devtools' | 'fintech' | 'enterprise'
      role: string
      metric: string
      region: string
    }
    
    export type LogoTickerCategory = 'all' | 'devtools' | 'fintech' | 'enterprise'
    
    const partners: TickerBrandPartner[] = [
      {
        id: 'vercel',
        name: 'Vercel Edge',
        category: 'devtools',
        role: 'Global Edge Ingestion',
        metric: '99.95% SLA',
        region: 'Global',
      },
      {
        id: 'supabase',
        name: 'Supabase Postgres',
        category: 'devtools',
        role: 'Real-Time Replication',
        metric: '1.2M QPS',
        region: 'US-East',
      },
      {
        id: 'linear',
        name: 'Linear Method',
        category: 'enterprise',
        role: 'Issue Sync Engine',
        metric: '< 20ms Sync',
        region: 'Global',
      },
      {
        id: 'stripe',
        name: 'Stripe Billing',
        category: 'fintech',
        role: 'Usage Metering Gateway',
        metric: '$120M+ Processed',
        region: 'Global',
      },
      {
        id: 'resend',
        name: 'Resend SMTP',
        category: 'devtools',
        role: 'Transactional Dispatch',
        metric: '200M msgs/mo',
        region: 'US-West',
      },
      {
        id: 'cloudflare',
        name: 'Cloudflare Workers',
        category: 'devtools',
        role: 'Zero-Trust Isolation',
        metric: '300+ Cities',
        region: 'Global',
      },
      {
        id: 'planetscale',
        name: 'PlanetScale DB',
        category: 'enterprise',
        role: 'Branching Schema',
        metric: 'Zero Downtime',
        region: 'EU-Central',
      },
      {
        id: 'brex',
        name: 'Brex Treasury',
        category: 'fintech',
        role: 'Ledger Reconciliation',
        metric: 'Real-time API',
        region: 'US-East',
      },
    ]
    
    const categoryFilters: LogoTickerCategory[] = ['all', 'devtools', 'fintech', 'enterprise']
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-logo-ticker-infinite, [ui-logo-ticker-infinite]',
      standalone: true,
      // React renders the root <section> itself: the host stays out of layout and `class` goes to the root.
      host: { '[attr.class]': '"contents"' },
      imports: [LucideAngularModule, UiBadgeComponent],
      styles: [
        `
          @keyframes ui-logo-ticker-marquee {
            0% {
              transform: translateX(0%);
            }
            100% {
              transform: translateX(-50%);
            }
          }
          .animate-marquee {
            animation: ui-logo-ticker-marquee 32s linear infinite;
          }
        `,
      ],
      template: `
        <section data-slot="logo-ticker-infinite" [class]="rootClass">
          <div class="mx-auto max-w-7xl space-y-8 text-center">
            <!-- Section Header & Category Filter Controls -->
            <div
              class="border-border/60 flex flex-col items-center justify-between gap-4 border-b pb-6 text-left md:flex-row"
            >
              <div class="space-y-1">
                <div class="flex items-center gap-2">
                  <span class="bg-success size-2 rounded-full"></span>
                  <p class="text-foreground font-mono text-xs font-semibold tracking-wider uppercase">
                    Production Verified Infrastructure
                  </p>
                </div>
                <p class="text-muted-foreground text-xs">
                  Trusted by modern engineering teams shipping high-throughput production workloads.
                </p>
              </div>
    
              <!-- Filter & Control Buttons -->
              <div class="flex flex-wrap items-center gap-2">
                <!-- Category Filter Pills -->
                <div class="bg-background border-border flex items-center gap-1 rounded-lg border p-0.5">
                  @for (cat of filters; track cat) {
                    <button type="button" [class]="filterPillClass(cat)" (click)="setCategory(cat)">
                      {{ cat }}
                    </button>
                  }
                </div>
    
                <!-- Marquee Speed / Pause Controls -->
                <div class="flex items-center gap-1">
                  <button
                    type="button"
                    class="border-border bg-background text-muted-foreground hover:text-foreground rounded-lg border p-1.5 transition-colors"
                    [attr.title]="isPaused() ? 'Resume Ticker' : 'Pause Ticker'"
                    [attr.aria-label]="isPaused() ? 'Resume Ticker' : 'Pause Ticker'"
                    (click)="togglePaused()"
                  >
                    @if (isPaused()) {
                      <lucide-icon [img]="Play" class="size-3.5 fill-current" />
                    } @else {
                      <lucide-icon [img]="Pause" class="size-3.5" />
                    }
                  </button>
                  <button
                    type="button"
                    class="border-border bg-background text-muted-foreground hover:text-foreground rounded-lg border px-2 py-1 font-mono text-xs transition-colors"
                    [attr.aria-label]="isFastSpeed() ? 'Normal speed' : 'Double speed'"
                    (click)="toggleSpeed()"
                  >
                    {{ isFastSpeed() ? '2x' : '1x' }}
                  </button>
                </div>
              </div>
            </div>
    
            <!-- Infinite Marquee Track Viewport with Gradient Fade Edges -->
            <div class="group relative overflow-hidden">
              <!-- Left & Right Gradient Shadows -->
              <div
                class="from-background via-background/80 pointer-events-none absolute inset-y-0 left-0 z-10 w-16 bg-gradient-to-r to-transparent sm:w-28"
              ></div>
              <div
                class="from-background via-background/80 pointer-events-none absolute inset-y-0 right-0 z-10 w-16 bg-gradient-to-l to-transparent sm:w-28"
              ></div>
    
              <!-- Double Track Container for Seamless Infinite Scrolling -->
              <div [class]="trackClass" [style.animation-play-state]="isPaused() ? 'paused' : 'running'">
                <!-- Repeat partners array twice for infinite seamless loop -->
                @for (item of loopedPartners(); track item.id + '-' + item.index) {
                  <div
                    class="border-border bg-card/80 hover:bg-card hover:border-primary/50 relative flex shrink-0 cursor-pointer items-center gap-3 rounded-xl border px-4 py-2.5 shadow-xs backdrop-blur-md transition-colors duration-200"
                    (mouseenter)="setHovered(item.partner)"
                    (mouseleave)="clearHovered()"
                  >
                    <div
                      class="bg-primary/10 text-primary flex size-7 items-center justify-center rounded-lg font-mono text-xs font-bold"
                    >
                      {{ item.partner.name.charAt(0) }}
                    </div>
    
                    <div class="text-left">
                      <p class="text-foreground font-mono text-xs font-semibold tracking-tight">{{ item.partner.name }}</p>
                      <p class="text-muted-foreground text-xs">{{ item.partner.role }}</p>
                    </div>
    
                    <span
                      ui-badge
                      variant="outline"
                      class="border-success/20 bg-success/10 text-success ml-1 font-mono text-xs"
                    >
                      {{ item.partner.metric }}
                    </span>
                  </div>
                }
              </div>
            </div>
    
            <!-- Live Hover Telemetry Card -->
            @if (hoveredBrand(); as hovered) {
              <div
                class="border-border bg-card animate-in fade-in-0 mx-auto max-w-md rounded-xl border p-3 text-left shadow-lg duration-150"
              >
                <div class="flex items-center justify-between font-mono text-xs">
                  <span class="text-foreground font-bold">{{ hovered.name }} Architecture Integration</span>
                  <span class="text-success">{{ hovered.region }}</span>
                </div>
                <p class="text-muted-foreground mt-1 text-xs">
                  Connected through {{ hovered.role }} &bull; Monitored with {{ hovered.metric }} verified telemetry.
                </p>
              </div>
            }
          </div>
        </section>
      `,
    })
    export class UiLogoTickerInfiniteComponent {
      protected readonly Pause = Pause
      protected readonly Play = Play
    
      protected readonly filters = categoryFilters
    
      @Input('class') className?: string
      @Input() set initialCategory(v: LogoTickerCategory | undefined) {
        if (v) this.selectedCategory.set(v)
      }
    
      readonly selectedCategory = signal<LogoTickerCategory>('all')
      readonly isPaused = signal(false)
      readonly isFastSpeed = signal(false)
      readonly hoveredBrand = signal<TickerBrandPartner | null>(null)
    
      readonly filteredPartners = computed(() => {
        const selected = this.selectedCategory()
        if (selected === 'all') return partners
        return partners.filter((p) => p.category === selected)
      })
    
      /** The filtered list twice, so the -50% marquee loop is seamless. */
      readonly loopedPartners = computed(() => {
        const list = this.filteredPartners()
        return [...list, ...list].map((partner, index) => ({ partner, index }))
      })
    
      get rootClass(): string {
        return cn(
          'border-border/80 bg-muted/20 relative overflow-hidden border-y px-4 py-14 sm:px-6 sm:py-20 lg:px-8',
          this.className,
        )
      }
    
      get trackClass(): string {
        return cn(
          'flex w-max items-center gap-4 py-3',
          !this.isPaused() && 'animate-marquee',
          this.isFastSpeed() ? '[animation-duration:15s]' : '[animation-duration:32s]',
        )
      }
    
      filterPillClass(cat: LogoTickerCategory): string {
        return cn(
          'rounded-md px-2.5 py-1 font-mono text-xs capitalize transition-colors',
          this.selectedCategory() === cat
            ? 'bg-primary text-primary-foreground font-semibold shadow-xs'
            : 'text-muted-foreground hover:text-foreground',
        )
      }
    
      setCategory(cat: LogoTickerCategory): void {
        this.selectedCategory.set(cat)
      }
    
      togglePaused(): void {
        this.isPaused.update((v) => !v)
      }
    
      toggleSpeed(): void {
        this.isFastSpeed.update((v) => !v)
      }
    
      setHovered(partner: TickerBrandPartner): void {
        this.hoveredBrand.set(partner)
      }
    
      clearHovered(): void {
        this.hoveredBrand.set(null)
      }
    }
    
  • src/app/components/blocks/logo-ticker-infinite/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/logo-ticker-infinite.json