Feature Hover Tilt Cards
blockmarketing3D perspective tilt workbench cards with dynamic mouse tracking, interactive token efficiency sliders, spring impulse testing, and dual-framework parity inspect.
Installation
$pnpm dlx uipkge-ng@latest add feature-hover-tilt-cards$npx uipkge-ng@latest add feature-hover-tilt-cards$yarn dlx uipkge-ng@latest add feature-hover-tilt-cards$bunx uipkge-ng@latest add feature-hover-tilt-cardsManifest:
https://uipkge.dev/r/angular/feature-hover-tilt-cards.jsonInstalls to:src/app/components/blocks/feature-hover-tilt-cards/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
CardRotationinterface 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 & Surface Depth </span> <h2 class="text-foreground text-3xl font-bold tracking-tight sm:text-4xl"> Engineered for tactile feedback & 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">✓ 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">✓ 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 & 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">✓ 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
export { UiFeatureHoverTiltCardsComponent, type CardRotation } from './feature-hover-tilt-cards.component'
Raw manifest:https://uipkge.dev/r/angular/feature-hover-tilt-cards.json