# Personal Hero

> Narrow personal intro with status, lede links, and meta.

A shadcn/ui block from the [@atroui](https://www.atroui.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/atroui/personal-hero).

- Page, with a live preview: https://shadcn.exchange/r/atroui/personal-hero
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/atroui/personal-hero-light-226eadb92b162b14.png
- Registry item: https://www.atroui.com/r/personal-hero.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://www.atroui.com/r/personal-hero.json
```

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

## Files

### `registry/default/blocks/personal-hero.tsx`

```tsx
const CONTENT = {
  name: "Your Name",
  tagline: "Designer-engineer building calm software.",
  location: "Somewhere on Earth",
  status: "shipping" as const,
  /** Set to your portrait path (e.g. `/me.jpg`) after install. */
  imageSrc: "",
  imageAlt: "",
  ledeLinks: [
    { label: "studio", href: "https://example.com" },
    { label: "writing", href: "/writing" },
  ],
  ledeBefore: "Shipping at ",
  ledeBetween: ", writing at ",
  ledeAfter: ".",
}

type StatusKind = "shipping" | "working" | "dormant"

const statusCopy: Record<StatusKind, string> = {
  shipping: "shipping",
  working: "working",
  dormant: "dormant",
}

const statusDot: Record<StatusKind, string> = {
  shipping: "bg-emerald-500",
  working: "bg-muted-foreground",
  dormant: "bg-muted-foreground/50",
}

export function PersonalHero({
  name = CONTENT.name,
  tagline = CONTENT.tagline,
  location = CONTENT.location,
  status = CONTENT.status,
  imageSrc = CONTENT.imageSrc,
  imageAlt = CONTENT.imageAlt,
  ledeLinks = CONTENT.ledeLinks,
  ledeBefore = CONTENT.ledeBefore,
  ledeBetween = CONTENT.ledeBetween,
  ledeAfter = CONTENT.ledeAfter,
  className,
}: {
  name?: string
  tagline?: string
  location?: string
  status?: StatusKind | null
  imageSrc?: string
  imageAlt?: string
  ledeLinks?: Array<{ label: string; href: string; external?: boolean }>
  ledeBefore?: string
  ledeBetween?: string
  ledeAfter?: string
  className?: string
} = {}) {
  const portrait = imageSrc.trim()

  return (
    <section
      className={className ?? "mx-auto max-w-[640px] pt-14 sm:pt-20"}
    >
      <div className="flex items-start justify-between gap-4">
        <div className="flex min-w-0 items-start gap-4">
          {portrait ? (
            <img
              src={portrait}
              alt={imageAlt.trim() || name}
              width={72}
              height={72}
              className="mt-1 size-18 shrink-0 rounded-full border border-border-subtle bg-muted object-cover"
            />
          ) : null}
          <h1 className="min-w-0 pt-1 text-[2rem] font-medium tracking-tight text-foreground sm:text-[2.5rem]">
            {name}
          </h1>
        </div>
        {status ? <StatusDot status={status} /> : null}
      </div>

      <p className="mt-5 max-w-[36ch] text-[15px] leading-[1.65] text-foreground">
        {tagline}
      </p>

      {ledeLinks.length > 0 ? (
        <p className="mt-1.5 max-w-[42ch] text-[14px] leading-[1.65] text-muted-foreground">
          {ledeBefore}
          {ledeLinks.map((link, i) => {
            const external =
              link.external ?? /^https?:\/\//.test(link.href)
            return (
              <span key={link.href}>
                {i > 0 ? ledeBetween : null}
                <a
                  href={link.href}
                  {...(external
                    ? { target: "_blank", rel: "noopener noreferrer" }
                    : {})}
                  className="text-foreground underline decoration-border-subtle underline-offset-[3px] transition-colors hover:decoration-foreground"
                >
                  {link.label}
                </a>
              </span>
            )
          })}
          {ledeAfter}
        </p>
      ) : null}

      <div className="mt-5 flex flex-wrap items-center gap-x-3 gap-y-1 font-mono text-[11.5px] text-muted-foreground">
        <span>{location}</span>
      </div>
    </section>
  )
}

function StatusDot({ status }: { status: StatusKind }) {
  return (
    <span
      className="inline-flex items-center gap-1.5 pt-2 font-mono text-[11px] text-muted-foreground"
      aria-label={`Status: ${statusCopy[status]}`}
    >
      <span className="relative inline-flex h-[7px] w-[7px] shrink-0">
        <span
          className={`absolute inset-0 rounded-full ${statusDot[status]} ${
            status === "shipping" ? "animate-ping opacity-60" : "opacity-0"
          }`}
          aria-hidden
        />
        <span
          className={`relative h-[7px] w-[7px] rounded-full ${statusDot[status]}`}
          aria-hidden
        />
      </span>
      {statusCopy[status]}
    </span>
  )
}
```
