Menu

Change language

GitHub repo

Feature Hover Tilt Cards

blockmarketing

3D perspective tilt workbench cards with dynamic mouse tracking, interactive token efficiency sliders, spring impulse testing, and dual-framework parity inspect.

Installation

$npx uipkge-ng@latest add feature-hover-tilt-cards
Manifest:https://uipkge.dev/r/angular/feature-hover-tilt-cards.jsonInstalls to:src/app/components/blocks/feature-hover-tilt-cards/

Variants

Loading interactive previews…

Schema

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

CardRotation
interface CardRotation {
  rx: number
  ry: number
}

npm dependencies

Files installed (2)

  • src/app/components/blocks/feature-hover-tilt-cards/feature-hover-tilt-cards.component.ts13 kB
    // Tactile 3D perspective tilt workbench: each card tracks the pointer for a hardware-accelerated
    // rotateX/rotateY depth effect, plus live interactives (token efficiency slider, spring impulse
    // trigger, dual-framework mirror switcher). Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, Input, signal } from '@angular/core'
    import { Cpu, Eye, Layers, LayoutGrid, LucideAngularModule, Zap } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import { UiCardComponent } from '@/ui/card/card.component'
    
    export interface CardRotation {
      rx: number
      ry: number
    }
    
    const NEUTRAL_ROTATIONS: Record<number, CardRotation> = {
      0: { rx: 0, ry: 0 },
      1: { rx: 0, ry: 0 },
      2: { rx: 0, ry: 0 },
    }
    
    // Kept out of the template: a literal `<script` inside an interpolation trips
    // prettier's HTML parser (it looks like a real tag open).
    const MIRROR_BADGES = {
      vue: '<script setup lang="ts"> + Reka UI',
      react: "'use client' + Radix UI",
    } as const
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-feature-hover-tilt-cards, [ui-feature-hover-tilt-cards]',
      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, UiButtonComponent, UiCardComponent],
      template: `
        <section data-slot="feature-hover-tilt-cards" [class]="rootClass">
          <div class="mx-auto max-w-7xl space-y-12">
            <!-- Section Header -->
            <div class="mx-auto max-w-3xl space-y-4 text-center">
              <span ui-badge variant="secondary" class="gap-1.5 px-3 py-1 font-mono text-xs shadow-xs">
                <lucide-icon [img]="Eye" class="text-primary size-3.5" />
                Tactile Physics &amp; Surface Depth
              </span>
              <h2 class="text-foreground text-3xl font-bold tracking-tight sm:text-4xl">
                Engineered for tactile feedback &amp; micro-interactions.
              </h2>
              <p class="text-muted-foreground text-base">
                Hover over each workbench card to experience hardware-accelerated 3D depth and live interactive physics.
              </p>
            </div>
    
            <!-- 3D Perspective Tilt Cards Grid -->
            <div class="grid grid-cols-1 gap-6 [perspective:1200px] lg:grid-cols-3">
              <!-- Card 1: Unbundled Token Inspector -->
              <div
                class="transition-transform duration-200 ease-out will-change-transform"
                [style.transform]="tiltTransform(0)"
                (mousemove)="handleMouseMove($event, 0)"
                (mouseleave)="handleMouseLeave(0)"
              >
                <div
                  ui-card
                  class="border-border bg-card/95 hover:border-primary/50 relative flex h-full flex-col justify-between space-y-6 overflow-hidden rounded-2xl p-6 text-left shadow-xl transition-[color,background-color,border-color,box-shadow] hover:shadow-sm"
                >
                  <div class="space-y-3">
                    <div class="flex items-center justify-between">
                      <div class="bg-primary/10 text-primary flex size-9 items-center justify-center rounded-xl font-mono">
                        <lucide-icon [img]="Layers" class="size-4" />
                      </div>
                      <span ui-badge variant="outline" class="font-mono text-xs">Zero Bloat</span>
                    </div>
    
                    <h3 class="text-foreground font-mono text-lg font-bold">Unbundled Code Ownership</h3>
                    <p class="text-muted-foreground text-xs leading-relaxed">
                      AST-level code generation puts pristine component source directly in your repository.
                    </p>
    
                    <!-- Interactive Token Scale Slider -->
                    <div class="border-border bg-muted/30 mt-4 space-y-2 rounded-xl border p-3.5">
                      <div class="flex items-center justify-between font-mono text-xs">
                        <span class="text-muted-foreground">Tree-Shake Efficiency:</span>
                        <span class="text-success font-bold">{{ tokenScale() }}%</span>
                      </div>
                      <input
                        type="range"
                        min="50"
                        max="100"
                        [value]="tokenScale()"
                        (input)="onTokenScaleInput($event)"
                        aria-label="Tree-shake efficiency"
                        class="accent-primary bg-border h-1.5 w-full cursor-pointer rounded-lg"
                      />
                      <p class="text-muted-foreground font-mono text-xs">
                        Unused variants automatically pruned at compile time.
                      </p>
                    </div>
                  </div>
    
                  <div
                    class="border-border/60 text-muted-foreground flex items-center justify-between border-t pt-4 font-mono text-xs"
                  >
                    <span>0 dependencies added</span>
                    <span class="text-primary font-semibold">&check; Pure Source</span>
                  </div>
                </div>
              </div>
    
              <!-- Card 2: Spring Velocity Simulator -->
              <div
                class="transition-transform duration-200 ease-out will-change-transform"
                [style.transform]="tiltTransform(1)"
                (mousemove)="handleMouseMove($event, 1)"
                (mouseleave)="handleMouseLeave(1)"
              >
                <div
                  ui-card
                  class="border-border bg-card/95 hover:border-primary/50 relative flex h-full flex-col justify-between space-y-6 overflow-hidden rounded-2xl p-6 text-left shadow-xl transition-[color,background-color,border-color,box-shadow] hover:shadow-sm"
                >
                  <div class="space-y-3">
                    <div class="flex items-center justify-between">
                      <div class="bg-warning/10 text-warning flex size-9 items-center justify-center rounded-xl font-mono">
                        <lucide-icon [img]="Zap" class="size-4" />
                      </div>
                      <span
                        ui-badge
                        variant="outline"
                        class="border-warning/20 bg-warning/10 text-warning font-mono text-xs"
                      >
                        Fluid Motion
                      </span>
                    </div>
    
                    <h3 class="text-foreground font-mono text-lg font-bold">Spring Physics Engine</h3>
                    <p class="text-muted-foreground text-xs leading-relaxed">
                      Calibrated cubic-bezier curves for tactile, zero-overshoot micro-interactions.
                    </p>
    
                    <!-- Interactive Spring Trigger Box -->
                    <div class="border-border bg-muted/30 mt-4 space-y-3 rounded-xl border p-3.5 text-center">
                      <div
                        [class]="springCubeClass"
                        class="bg-primary text-primary-foreground mx-auto flex size-12 items-center justify-center rounded-2xl font-mono text-sm font-bold shadow-md transition-colors duration-300"
                      >
                        <lucide-icon [img]="Cpu" class="size-5" />
                      </div>
    
                      <button
                        ui-button
                        size="sm"
                        variant="outline"
                        class="h-8 w-full font-mono text-xs"
                        (click)="triggerSpringTest()"
                      >
                        Trigger Spring Impulse
                      </button>
                    </div>
                  </div>
    
                  <div
                    class="border-border/60 text-muted-foreground flex items-center justify-between border-t pt-4 font-mono text-xs"
                  >
                    <span>Duration: 180ms</span>
                    <span class="text-success font-semibold">&check; 60fps Native</span>
                  </div>
                </div>
              </div>
    
              <!-- Card 3: Dual-Framework Mirror Comparator -->
              <div
                class="transition-transform duration-200 ease-out will-change-transform"
                [style.transform]="tiltTransform(2)"
                (mousemove)="handleMouseMove($event, 2)"
                (mouseleave)="handleMouseLeave(2)"
              >
                <div
                  ui-card
                  class="border-border bg-card/95 hover:border-primary/50 relative flex h-full flex-col justify-between space-y-6 overflow-hidden rounded-2xl p-6 text-left shadow-xl transition-[color,background-color,border-color,box-shadow] hover:shadow-sm"
                >
                  <div class="space-y-3">
                    <div class="flex items-center justify-between">
                      <div class="bg-info/10 text-info flex size-9 items-center justify-center rounded-xl font-mono">
                        <lucide-icon [img]="LayoutGrid" class="size-4" />
                      </div>
                      <span ui-badge variant="outline" class="border-info/20 bg-info/10 text-info font-mono text-xs">
                        1:1 Parity
                      </span>
                    </div>
    
                    <h3 class="text-foreground font-mono text-lg font-bold">Dual-Framework Compiler</h3>
                    <p class="text-muted-foreground text-xs leading-relaxed">
                      Identical DOM attributes, variants, and keyboard ergonomics across Vue 3.5 &amp; React 19.
                    </p>
    
                    <!-- Interactive Framework Switcher -->
                    <div class="border-border bg-muted/30 mt-4 space-y-2 rounded-xl border p-3 font-mono text-xs">
                      <div class="bg-background border-border flex items-center gap-1 rounded-lg border p-1">
                        <button
                          type="button"
                          [class]="mirrorTabClass('vue')"
                          class="flex-1 rounded py-1 text-center transition-colors"
                          (click)="setActiveMirror('vue')"
                        >
                          Vue 3.5 SFC
                        </button>
                        <button
                          type="button"
                          [class]="mirrorTabClass('react')"
                          class="flex-1 rounded py-1 text-center transition-colors"
                          (click)="setActiveMirror('react')"
                        >
                          React 19 JSX
                        </button>
                      </div>
                      <p class="text-muted-foreground pt-1 text-xs">{{ mirrorBadge }}</p>
                    </div>
                  </div>
    
                  <div
                    class="border-border/60 text-muted-foreground flex items-center justify-between border-t pt-4 font-mono text-xs"
                  >
                    <span>AST Verified</span>
                    <span class="text-primary font-semibold">&check; 100% Match</span>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </section>
      `,
    })
    export class UiFeatureHoverTiltCardsComponent {
      protected readonly Cpu = Cpu
      protected readonly Eye = Eye
      protected readonly Layers = Layers
      protected readonly LayoutGrid = LayoutGrid
      protected readonly Zap = Zap
    
      @Input('class') className?: string
    
      readonly rotations = signal<Record<number, CardRotation>>({ ...NEUTRAL_ROTATIONS })
      readonly tokenScale = signal(100)
      readonly isPulsing = signal(false)
      readonly activeMirror = signal<'vue' | 'react'>('vue')
      private pulseTimer: ReturnType<typeof setTimeout> | undefined
    
      get rootClass(): string {
        return cn('bg-background relative overflow-hidden px-4 py-16 sm:px-6 sm:py-24 lg:px-8', this.className)
      }
    
      get mirrorBadge(): string {
        return MIRROR_BADGES[this.activeMirror()]
      }
    
      get springCubeClass(): string {
        return cn(this.isPulsing() ? 'bg-success ring-success/30 scale-125 rotate-12 ring-4' : 'scale-100')
      }
    
      tiltTransform(index: number): string {
        const r = this.rotations()[index] ?? { rx: 0, ry: 0 }
        return `perspective(1000px) rotateX(${r.rx}deg) rotateY(${r.ry}deg)`
      }
    
      mirrorTabClass(mirror: 'vue' | 'react'): string {
        return cn(
          this.activeMirror() === mirror
            ? 'bg-primary text-primary-foreground font-semibold shadow-xs'
            : 'text-muted-foreground',
        )
      }
    
      handleMouseMove(event: MouseEvent, index: number): void {
        const card = event.currentTarget as HTMLElement | null
        const rect = card?.getBoundingClientRect()
        if (!rect || rect.width === 0 || rect.height === 0) return
        const x = event.clientX - rect.left
        const y = event.clientY - rect.top
        const centerX = rect.width / 2
        const centerY = rect.height / 2
        const rotateX = ((y - centerY) / centerY) * -7
        const rotateY = ((x - centerX) / centerX) * 7
        this.rotations.update((prev) => ({ ...prev, [index]: { rx: rotateX, ry: rotateY } }))
      }
    
      handleMouseLeave(index: number): void {
        this.rotations.update((prev) => ({ ...prev, [index]: { rx: 0, ry: 0 } }))
      }
    
      onTokenScaleInput(event: Event): void {
        this.tokenScale.set(Number((event.target as HTMLInputElement).value))
      }
    
      triggerSpringTest(): void {
        this.isPulsing.set(true)
        clearTimeout(this.pulseTimer)
        this.pulseTimer = setTimeout(() => this.isPulsing.set(false), 600)
      }
    
      setActiveMirror(mirror: 'vue' | 'react'): void {
        this.activeMirror.set(mirror)
      }
    }
    
  • src/app/components/blocks/feature-hover-tilt-cards/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/feature-hover-tilt-cards.json