# Profile card navbar

A shadcn/ui block from the [@craftui](https://www.craftui.space) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/craftui/profile-card-navbar).

- Page, with a live preview: https://shadcn.exchange/r/craftui/profile-card-navbar
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/craftui/profile-card-navbar-light-11c6cd3b9056d112.png
- Registry item: https://www.craftui.space/r/profile-card-navbar.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://www.craftui.space/r/profile-card-navbar.json
```

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

## Dependencies

- npm packages: `@hugeicons/core-free-icons`, `@hugeicons/react`, `motion`
- Registry items: `https://www.craftui.space/r/craftui-base.json`

## Files

### `components/craftui/blocks/profile-card/navbar.tsx`

```tsx
"use client";

import React from "react";
import { motion, AnimatePresence } from "motion/react";
import { HugeiconsIcon } from "@hugeicons/react";
import {
  CheckmarkBadge01Icon,
  UserGroupIcon,
  CreditCardIcon,
  Settings01Icon,
  HelpCircleIcon,
  Logout01Icon,
  FlashIcon,
} from "@hugeicons/core-free-icons";

// ── Types ─────────────────────────────────────────────────────────────────────

type HugeIcon = React.ComponentProps<typeof HugeiconsIcon>["icon"];

export type MenuItem = {
  label: string;
  icon: HugeIcon;
  badge?: React.ReactNode;
  active?: boolean;
  onClick?: () => void;
  /** Renders a divider below this item — used to visually group rows. */
  dividerAfter?: boolean;
};

export interface ProfileCardProps {
  name?: string;
  email?: string;
  /** Avatar slot — defaults to DefaultAvatar. Pass an <img>, next/image, or any node. */
  avatar?: React.ReactNode;
  menuItems?: MenuItem[];
  className?: string;
}

// ── Default content ──────────────────────────────────────────────────────────

const DEFAULT_MENU_ITEMS: MenuItem[] = [
  { label: "Profile", icon: CheckmarkBadge01Icon, active: true },
  {
    label: "Community",
    icon: UserGroupIcon,
    badge: (
      <span className="flex size-5 items-center justify-center rounded-full border border-black/15 text-[10px] font-semibold text-black/50">
        +
      </span>
    ),
  },
  {
    label: "Subscription",
    icon: CreditCardIcon,
    badge: (
      <span className="flex items-center gap-0.5 rounded-full bg-green-400 px-2 py-0.5 text-[10px] font-bold text-black">
        <HugeiconsIcon
          icon={FlashIcon}
          size={10}
          color="black"
          strokeWidth={2.5}
        />
        PRO
      </span>
    ),
  },
  { label: "Settings", icon: Settings01Icon, dividerAfter: true },
  { label: "Help center", icon: HelpCircleIcon },
  { label: "Sign out", icon: Logout01Icon },
];

const SPRING = { type: "spring", duration: 0.5, bounce: 0.25 } as const;

/** Splits a flat item list into groups, breaking after each `dividerAfter` item. */
function splitIntoSegments(items: MenuItem[]): MenuItem[][] {
  const segments: MenuItem[][] = [[]];
  for (const item of items) {
    segments[segments.length - 1].push(item);
    if (item.dividerAfter) segments.push([]);
  }
  return segments.filter((segment) => segment.length > 0);
}

// ── Avatar ────────────────────────────────────────────────────────────────────

export function DefaultAvatar({ size }: { size: number }) {
  return (
    <div
      style={{ width: size, height: size, borderRadius: size / 2 }}
      className="flex flex-shrink-0 items-center justify-center overflow-hidden bg-gradient-to-br from-blue-400 via-purple-400 to-pink-300"
    >
      <svg viewBox="0 0 60 60" width={size * 0.8} height={size * 0.8}>
        <ellipse cx="30" cy="34" rx="18" ry="16" fill="#5b7be9" />
        <ellipse cx="30" cy="20" rx="14" ry="14" fill="#6b8ef0" />
        <circle cx="24" cy="20" r="3.5" fill="white" />
        <circle cx="36" cy="20" r="3.5" fill="white" />
        <circle cx="25" cy="21" r="1.8" fill="#1a1a2e" />
        <circle cx="37" cy="21" r="1.8" fill="#1a1a2e" />
        <ellipse cx="30" cy="29" rx="5" ry="3" fill="#f87171" />
      </svg>
    </div>
  );
}

// ── MenuRow ───────────────────────────────────────────────────────────────────

function MenuRow({ item }: { item: MenuItem }) {
  return (
    <motion.button
      whileTap={{ scale: 0.98 }}
      onClick={item.onClick}
      className={`flex w-full cursor-pointer items-center gap-3 rounded-xl px-3 py-2.5 text-left transition-colors select-none ${
        item.active ? "bg-black/[0.06]" : "hover:bg-black/[0.04]"
      }`}
    >
      <span className="flex-shrink-0 text-black/50">
        <HugeiconsIcon
          icon={item.icon}
          size={18}
          color="currentColor"
          strokeWidth={1.6}
        />
      </span>
      <span className="flex-1 text-[15px] font-medium text-black/85 antialiased">
        {item.label}
      </span>
      {item.badge}
    </motion.button>
  );
}

// ── Root ──────────────────────────────────────────────────────────────────────

export default function ProfileCard({
  name = "Sophie Bennett",
  email = "sophie@ui.live",
  avatar,
  menuItems = DEFAULT_MENU_ITEMS,
  className,
}: ProfileCardProps = {}) {
  const [isOpen, setIsOpen] = React.useState(false);
  const [menuReady, setMenuReady] = React.useState(false);

  React.useEffect(() => {
    if (!isOpen) {
      setMenuReady(false);
      return;
    }
    // delay + duration of the fade wrapper animation (0.18 + 0.2 = 0.38s)
    const id = setTimeout(() => setMenuReady(true), 400);
    return () => clearTimeout(id);
  }, [isOpen]);

  const collapsedAvatar = avatar ?? <DefaultAvatar size={36} />;
  const expandedAvatar = avatar ?? <DefaultAvatar size={42} />;

  return (
    <div className={`relative h-full w-full ${className ?? ""}`}>
      {/* Dismiss overlay — scoped to whatever container this is placed in */}
      <AnimatePresence>
        {isOpen && (
          <motion.div
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            transition={{ duration: 0.15 }}
            onClick={() => setIsOpen(false)}
            className="absolute inset-0 z-10"
          />
        )}
      </AnimatePresence>

      {/* Avatar trigger — top-right of the container */}
      <div className="absolute top-3 right-5 z-20">
        <motion.div
          layoutId="profile-card"
          onClick={() => setIsOpen(true)}
          transition={SPRING}
          style={{ borderRadius: 20, pointerEvents: isOpen ? "none" : "auto" }}
          className="cursor-pointer"
        >
          <div style={{ width: 40, height: 40, position: "relative" }}>
            <motion.div
              layoutId="profile-avatar"
              transition={SPRING}
              className="absolute inset-[2px] overflow-hidden rounded-full"
            >
              {collapsedAvatar}
            </motion.div>
          </div>
        </motion.div>
      </div>

      {/* Expanded profile card */}
      <AnimatePresence>
        {isOpen && (
          <motion.div
            layoutId="profile-card"
            key="profile-card-expanded"
            onClick={() => setIsOpen(false)}
            transition={SPRING}
            style={{ borderRadius: 20 }}
            className="absolute top-3 right-5 z-20 w-[310px] max-w-[calc(100%-2.5rem)] cursor-pointer bg-white shadow-[0_8px_40px_rgba(0,0,0,0.14)]"
          >
            {/* Avatar — outside fade wrapper so it animates independently */}
            <div
              className="absolute top-4 right-4 cursor-pointer"
              style={{ width: 48, height: 48 }}
            >
              <motion.div
                layoutId="profile-avatar"
                transition={SPRING}
                className="absolute inset-[3px] overflow-hidden rounded-full"
              >
                {expandedAvatar}
              </motion.div>
            </div>

            {/* Fade wrapper — text + menus only */}
            <motion.div
              initial={{ opacity: 0, filter: "blur(4px)" }}
              animate={{ opacity: 1, filter: "blur(0px)" }}
              exit={{ opacity: 0, filter: "blur(4px)" }}
              transition={{ duration: 0.2, ease: "easeOut", delay: 0.18 }}
            >
              <div className="px-4 pt-4 pr-16 pb-3">
                <p className="text-[16px] font-bold tracking-tight text-black antialiased">
                  {name}
                </p>
                <p className="mt-0.5 text-xs text-black/40 antialiased">
                  {email}
                </p>
              </div>

              <div className="mx-1 h-px bg-black/[0.06]" />

              <div className={menuReady ? undefined : "pointer-events-none"}>
                {splitIntoSegments(menuItems).map((segment, i) => (
                  <React.Fragment key={i}>
                    {i > 0 && <div className="mx-1 h-px bg-black/[0.06]" />}
                    <div className="flex flex-col p-2">
                      {segment.map((item) => (
                        <MenuRow key={item.label} item={item} />
                      ))}
                    </div>
                  </React.Fragment>
                ))}
              </div>
            </motion.div>
          </motion.div>
        )}
      </AnimatePresence>
    </div>
  );
}
```
