# V sheet 5

> Shopping Cart

A shadcn/ui block from the [@cnippet](https://ui.cnippet.dev/) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/cnippet/v-sheet-5).

- Page, with a live preview: https://shadcn.exchange/r/cnippet/v-sheet-5
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/cnippet/v-sheet-5-light-336b65d5370a1e2c.png
- Registry item: https://ui.cnippet.dev/r/v-sheet-5.json
- Categories: sheet
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://ui.cnippet.dev/r/v-sheet-5.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: `@cnippet/sheet`

## Files

### `registry/default/variants/v-sheet-5.tsx`

```tsx
"use client";

import { MinusIcon, PlusIcon, ShoppingCartIcon, TrashIcon } from "lucide-react";
import { useState } from "react";
import { Badge } from "@/registry/default/ui/badge";
import { Button } from "@/registry/default/ui/button";
import {
  Sheet,
  SheetClose,
  SheetDescription,
  SheetFooter,
  SheetHeader,
  SheetPanel,
  SheetPopup,
  SheetTitle,
  SheetTrigger,
} from "@/registry/default/ui/sheet";

type CartItem = {
  id: number;
  name: string;
  price: number;
  qty: number;
  color: string;
};

const initial: CartItem[] = [
  {
    color: "bg-slate-400",
    id: 1,
    name: "Wireless Headphones",
    price: 79,
    qty: 1,
  },
  {
    color: "bg-violet-400",
    id: 2,
    name: "Mechanical Keyboard",
    price: 129,
    qty: 1,
  },
  { color: "bg-rose-400", id: 3, name: "USB-C Hub", price: 49, qty: 2 },
];

export function Pattern() {
  const [items, setItems] = useState<CartItem[]>(initial);

  const update = (id: number, delta: number) =>
    setItems((prev) =>
      prev
        .map((item) =>
          item.id === id ? { ...item, qty: item.qty + delta } : item,
        )
        .filter((item) => item.qty > 0),
    );

  const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0);
  const count = items.reduce((sum, item) => sum + item.qty, 0);

  return (
    <div className="flex items-center justify-center">
      <Sheet>
        <SheetTrigger render={<Button variant="outline" />}>
          <ShoppingCartIcon aria-hidden="true" />
          Cart
          {count > 0 && (
            <Badge className="ml-0.5 h-4 px-1 text-[10px]">{count}</Badge>
          )}
        </SheetTrigger>
        <SheetPopup>
          <SheetHeader>
            <SheetTitle>Shopping Cart</SheetTitle>
            <SheetDescription>
              {count} {count === 1 ? "item" : "items"} in your cart.
            </SheetDescription>
          </SheetHeader>
          <SheetPanel className="space-y-3">
            {items.length === 0 ? (
              <div className="flex flex-col items-center gap-2 py-10 text-center text-muted-foreground">
                <ShoppingCartIcon className="size-8 opacity-40" />
                <p className="text-sm">Your cart is empty.</p>
              </div>
            ) : (
              items.map((item) => (
                <div
                  className="flex items-center gap-3 rounded-lg border p-3"
                  key={item.id}
                >
                  <span
                    className={`size-10 shrink-0 rounded-md ${item.color}`}
                  />
                  <div className="min-w-0 flex-1">
                    <p className="truncate font-medium text-sm">{item.name}</p>
                    <p className="text-muted-foreground text-xs">
                      ${item.price.toFixed(2)} each
                    </p>
                  </div>
                  <div className="flex items-center gap-1">
                    <Button
                      aria-label="Decrease quantity"
                      className="size-6"
                      onClick={() => update(item.id, -1)}
                      size="icon"
                      variant="ghost"
                    >
                      {item.qty === 1 ? (
                        <TrashIcon className="size-3" />
                      ) : (
                        <MinusIcon className="size-3" />
                      )}
                    </Button>
                    <span className="w-5 text-center text-sm tabular-nums">
                      {item.qty}
                    </span>
                    <Button
                      aria-label="Increase quantity"
                      className="size-6"
                      onClick={() => update(item.id, 1)}
                      size="icon"
                      variant="ghost"
                    >
                      <PlusIcon className="size-3" />
                    </Button>
                  </div>
                </div>
              ))
            )}
          </SheetPanel>
          <SheetFooter className="flex-col gap-3 sm:flex-col">
            <div className="flex w-full items-center justify-between border-t pt-3 text-sm">
              <span className="text-muted-foreground">Subtotal</span>
              <span className="font-semibold">${subtotal.toFixed(2)}</span>
            </div>
            <div className="flex gap-2">
              <SheetClose
                render={<Button className="flex-1" variant="ghost" />}
              >
                Continue shopping
              </SheetClose>
              <Button className="flex-1" disabled={items.length === 0}>
                Checkout
              </Button>
            </div>
          </SheetFooter>
        </SheetPopup>
      </Sheet>
    </div>
  );
}
```
