# Widget 2

> An interactive seat selector for cinema and aircraft cabin booking, featuring hover details, status indicators, and total price calculation.

A shadcn/ui block from the [@paceui](https://paceui.com/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/paceui/dashboard-widget-2).

- Page, with a live preview: https://shadcn.exchange/r/paceui/dashboard-widget-2
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/paceui/dashboard-widget-2-light-d5493c855aee8c8d.png
- Registry item: https://paceui.com/r/dashboard-widget-2.json
- Listed: 2026-10-21, source last changed 2026-10-21

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/paceui/dashboard-widget-2.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `card`, `badge`

## Files

### `components/blocks/dashboard/widget/widget-2.tsx`

```tsx
import { useState } from "react";

import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";

type SeatStatus = "available" | "preferred" | "vip" | "accessible" | "reserved";

export const Widget2 = () => {
    const rows = ["A", "B", "C", "D", "E", "F", "G", "H"];
    const cols = Array.from({ length: 14 }, (_, i) => i + 1);

    const getInitialStatus = (row: string, col: number): SeatStatus => {
        const isReserved =
            (row === "A" && (col === 3 || col === 4)) ||
            (row === "B" && (col === 7 || col === 8)) ||
            (row === "C" && (col === 2 || col === 11)) ||
            (row === "D" && (col === 5 || col === 6)) ||
            (row === "E" && (col === 9 || col === 10));

        if (isReserved) return "reserved";

        if (row === "A" && (col === 1 || col === 12)) return "accessible";
        if (row === "B" && (col === 1 || col === 12)) return "accessible";

        if (row === "E" || row === "F") return "vip";

        if (row === "C" || row === "D") return "preferred";

        return "available";
    };

    const [selectedSeats, setSelectedSeats] = useState<string[]>([]);
    const [hoveredSeat, setHoveredSeat] = useState<{ row: string; col: number; status: SeatStatus } | null>(null);

    const toggleSeat = (row: string, col: number, status: SeatStatus) => {
        if (status === "reserved") return;
        const seatId = `${row}${col}`;
        setSelectedSeats((prev) => (prev.includes(seatId) ? prev.filter((s) => s !== seatId) : [...prev, seatId]));
    };

    const getSeatClasses = (row: string, col: number, status: SeatStatus) => {
        const seatId = `${row}${col}`;
        const isSelected = selectedSeats.includes(seatId);

        if (isSelected) {
            return "border-chart-2 bg-chart-2/70 scale-110";
        }

        switch (status) {
            case "reserved":
                return "border-border bg-muted/40 cursor-not-allowed opacity-40";
            case "vip":
                return "border-chart-4 bg-chart-4/15 hover:border-chart-4 hover:bg-chart-4/25 hover:scale-110";
            case "preferred":
                return "border-chart-1 bg-chart-1/15 hover:border-chart-1 hover:bg-chart-1/25 hover:scale-110";
            case "accessible":
                return "border-chart-5 bg-chart-5/15 hover:border-chart-5 hover:bg-chart-5/25 hover:scale-110";
            case "available":
            default:
                return "border-chart-3 bg-chart-3/15 hover:border-chart-3 hover:bg-chart-3/25 hover:scale-110";
        }
    };

    const getSeatPrice = (seatId: string) => {
        const row = seatId.charAt(0);
        const col = parseInt(seatId.slice(1));

        if (row === "A" && (col === 1 || col === 12)) return 10;
        if (row === "B" && (col === 1 || col === 12)) return 10;
        if (row === "E" || row === "F") return 22;
        if (row === "C" || row === "D") return 16;
        return 12;
    };

    const totalPrice = selectedSeats.reduce((sum, seat) => sum + getSeatPrice(seat), 0);

    return (
        <Card>
            <CardHeader className="flex justify-between">
                <div>
                    <CardTitle className="text-lg">Seat Booking</CardTitle>
                    <CardDescription>Select your preferred seats</CardDescription>
                </div>
                <Badge variant="outline">Stage 4</Badge>
            </CardHeader>

            <CardContent className="flex flex-col gap-6">
                <div className="flex flex-col items-center gap-2">
                    <div className="via-foreground/10 h-1.5 w-full rounded-full bg-linear-to-r from-transparent to-transparent" />
                    <span className="text-muted-foreground text-xs tracking-widest uppercase">Event</span>
                </div>

                <div className="grid grid-cols-14 justify-center gap-1.5 px-1 py-2">
                    {rows.map((row) =>
                        cols.map((col) => {
                            const status = getInitialStatus(row, col);
                            return (
                                <button
                                    key={`${row}-${col}`}
                                    onClick={() => toggleSeat(row, col, status)}
                                    onMouseEnter={() => setHoveredSeat({ row, col, status })}
                                    onMouseLeave={() => setHoveredSeat(null)}
                                    className={`size-4.5 cursor-pointer rounded-md border-2 transition-all duration-200 focus:outline-none ${getSeatClasses(row, col, status)}`}
                                />
                            );
                        }),
                    )}
                </div>

                <div className="flex h-4 items-center justify-center">
                    {hoveredSeat ? (
                        <span className="text-xs">
                            Seat {hoveredSeat.row}
                            {hoveredSeat.col} •{" "}
                            <span
                                className={
                                    hoveredSeat.status === "reserved"
                                        ? ""
                                        : selectedSeats.includes(`${hoveredSeat.row}${hoveredSeat.col}`)
                                          ? "text-chart-2 animate-pulse"
                                          : hoveredSeat.status === "vip"
                                            ? "text-chart-4"
                                            : hoveredSeat.status === "preferred"
                                              ? "text-chart-1"
                                              : hoveredSeat.status === "accessible"
                                                ? "text-chart-5"
                                                : "text-chart-3"
                                }>
                                {hoveredSeat.status === "reserved"
                                    ? "Reserved"
                                    : selectedSeats.includes(`${hoveredSeat.row}${hoveredSeat.col}`)
                                      ? "Selected"
                                      : hoveredSeat.status === "vip"
                                        ? "VIP ($22)"
                                        : hoveredSeat.status === "preferred"
                                          ? "Preferred ($16)"
                                          : hoveredSeat.status === "accessible"
                                            ? "Accessible ($10)"
                                            : "Standard ($12)"}
                            </span>
                        </span>
                    ) : (
                        <span className="animate-pulse text-xs">Hover seats to view details</span>
                    )}
                </div>

                <div className="grid grid-cols-3 gap-2 px-2 text-xs">
                    <div className="flex items-center gap-1.5">
                        <span className="bg-muted/40 size-2.5 rounded-md border" />
                        <span>Reserved</span>
                    </div>
                    <div className="flex items-center gap-1.5">
                        <span className="border-chart-3 bg-chart-3/15 size-2.5 rounded-md border" />
                        <span>Standard ($12)</span>
                    </div>
                    <div className="flex items-center gap-1.5">
                        <span className="border-chart-1 bg-chart-1/15 size-2.5 rounded-md border" />
                        <span>Preferred ($16)</span>
                    </div>
                    <div className="flex items-center gap-1.5">
                        <span className="border-chart-4 bg-chart-4/15 size-2.5 rounded-md border" />
                        <span>VIP ($22)</span>
                    </div>
                    <div className="flex items-center gap-1.5">
                        <span className="border-chart-5 bg-chart-5/15 size-2.5 rounded-md border" />
                        <span>Accessible ($10)</span>
                    </div>
                    <div className="flex items-center gap-1.5">
                        <span className="border-chart-2 bg-chart-2/25 size-2.5 rounded-md border" />
                        <span className="text-chart-2">Selected</span>
                    </div>
                </div>

                <div className="grid grid-cols-2 gap-4">
                    <div className="flex flex-col gap-1.5">
                        <span>Selected Seats</span>
                        <div className="flex items-center gap-2">
                            <div className="bg-chart-2 h-7 w-1 rounded-md" />
                            <span className="text-3xl">{selectedSeats.length}</span>
                        </div>
                    </div>

                    {/* Total Price */}
                    <div className="flex flex-col gap-1.5">
                        <span>Total Price</span>
                        <div className="flex items-center gap-2">
                            <div className="bg-chart-3 h-7 w-1 rounded-md" />
                            <span className="text-3xl">${totalPrice}</span>
                        </div>
                    </div>
                </div>
            </CardContent>
        </Card>
    );
};
```
