Componentticket-stub1.08 KB gzipZero dependencies
Trophy Shelf
Trophy shelf with earned/empty slots and drop-in animation.
Preview
Live Preview
Trophy2026
1/1 earned
Installation
Tokens guide →Add to your project via CLI (zero dependencies, code you own):
npx bigbullui add trophy-shelfOr install the full npm package:
npm install bigbulluiUsage
Open in StackBlitzimport { TrophyShelf } from "@/components/ui/trophy-shelf";
<TrophyShelf />Usage guidance
Use Trophy Shelf when a ticket stub surface needs Trophy shelf with earned/empty slots and drop-in animation. Keep props minimal and prefer composition with sibling stubs.
Avoid Trophy Shelf for long form flows or dense data grids where a dedicated layout block fits better.
Source code
trophy-shelf.tsxZero dependencies (only React + utils). Copy and paste directly into your project:
"use client";
import * as React from "react";
import { cn } from "./lib/utils";
export interface TrophyShelfProps extends React.HTMLAttributes<HTMLDivElement> {
trophies: { id: string; label: string; year: string; earned?: boolean }[];
}
/** Trophy shelf display: earned awards + shine overlay + open slots. */
export function TrophyShelf({ trophies, className, ...props }: TrophyShelfProps) {
return (
<div className={cn("w-full max-w-sm", className)} {...props}>
<style>{`@keyframes trIn { 0% { transform: translateY(-14px) scale(0.8); opacity: 0; } 70% { transform: translateY(2px) scale(1.05); opacity: 1; } 100% { transform: translateY(0) scale(1); opacity: 1; } }`}</style>
<div className="rounded-lg border-2 border-dashed border-border bg-card p-4">
<div className="flex items-end justify-around gap-2">
{trophies.map((t, idx) => (
<div key={t.id} className="flex flex-col items-center gap-1" style={{ width: `${100 / trophies.length}%` }}>
<span
className={cn("text-3xl", !t.earned && "opacity-25 grayscale")}
style={t.earned ? { animation: `trIn 0.5s cubic-bezier(0.34,1.56,0.64,1) both`, animationDelay: `${idx * 120}ms` } : undefined}
aria-hidden="true"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="size-7">
<path d="M8 4h8v4a4 4 0 0 1-8 0V4Z" />
<path d="M8 5.5H5.5a2 2 0 0 0 2 4H8M16 5.5h2.5a2 2 0 0 1-2 4H16" />
<path d="M12 12v3.5" />
<path d="M9 19.5h6" />
<path d="M9.5 15.5h5V19h-5v-3.5Z" />
<path d="M7.5 21.5h9" strokeDasharray="2 1.6" />
</svg>
</span>
<span className={cn("h-1 w-full rounded-full", t.earned ? "bg-accent" : "bg-border/40")} aria-hidden="true" />
<span className={cn("text-center text-[9px] font-medium leading-tight", t.earned ? "text-foreground" : "text-muted-foreground")}>{t.label}</span>
<span className="font-mono text-[8px] text-muted-foreground">{t.year}</span>
</div>
))}
</div>
<p className="mt-2 text-center font-mono text-[9px] uppercase tracking-widest text-muted-foreground">
{trophies.filter((t) => t.earned).length}/{trophies.length} earned
</p>
</div>
</div>
);
}
Props
| Prop | Type | Description |
|---|---|---|
| trophies | { id: string; label: string; year: string; earned?: boolean }[] |