{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "profile-card",
  "type": "registry:ui",
  "title": "Profile Card",
  "description": "A person with presence, local time and stats, and an optimistic follow that rolls back.",
  "categories": [
    "display"
  ],
  "dependencies": [
    "@base-ui/react",
    "@number-flow/react",
    "motion"
  ],
  "registryDependencies": [
    "https://www.stealth.pm/r/tokens.json",
    "https://www.stealth.pm/r/cn.json",
    "https://www.stealth.pm/r/icons.json",
    "https://www.stealth.pm/r/motion.json"
  ],
  "files": [
    {
      "path": "components/ui/profile-card.tsx",
      "content": "\"use client\";\nimport { Avatar } from \"@base-ui/react/avatar\";\nimport NumberFlow from \"@number-flow/react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { ease, spring } from \"@/lib/motion\";\nimport { Clock, Globe, Message, Pencil, Plus, X } from \"@/lib/icons\";\n\nexport type Presence = \"online\" | \"away\" | \"busy\" | \"offline\";\nexport const presenceLabel: Record<Presence, string> = { online: \"Online\", away: \"Away\", busy: \"Do not disturb\", offline: \"Offline\" };\n\n/* -------------------------------------------------------------------------------------------------\n * useFollow: optimistic follow with rollback\n * -----------------------------------------------------------------------------------------------*/\n\nexport type UseFollowOptions = {\n  following?: boolean;\n  defaultFollowing?: boolean;\n  /** Called with the new value. Return a promise to keep the change only if it resolves. */\n  onFollowingChange?: (following: boolean) => void | Promise<unknown>;\n  /** The follower count as it was when the card mounted. The local follow adds or removes one. */\n  followers?: number;\n};\n\n/**\n * Follow flips on the same frame and the count moves with it. The flip is an optimistic layer\n * over the confirmed value (your `following` prop, or internal state): if the request fails the\n * layer drops and both snap back, and `error` says which way it failed. Rapid presses settle on\n * the last one, and a late answer never overwrites a newer confirmed one.\n */\nexport function useFollow({ following: controlled, defaultFollowing = false, onFollowingChange, followers }: UseFollowOptions) {\n  const [inner, setInner] = useState(defaultFollowing);\n  const confirmed = controlled ?? inner;\n  const [optimistic, setOptimistic] = useState<boolean | null>(null);\n  const following = optimistic ?? confirmed;\n  const [initial] = useState(confirmed);\n  const [error, setError] = useState<\"follow\" | \"unfollow\" | null>(null);\n  const request = useRef(0);\n  const committed = useRef(0);\n  const clear = useRef<number>(undefined);\n  const handler = useRef(onFollowingChange);\n  useEffect(() => {\n    handler.current = onFollowingChange;\n  });\n  useEffect(() => () => window.clearTimeout(clear.current), []);\n\n  const toggle = useCallback(() => {\n    const next = !following;\n    const id = ++request.current;\n    setError(null);\n    window.clearTimeout(clear.current);\n    const result = handler.current?.(next);\n    if (!result || typeof (result as Promise<unknown>).then !== \"function\") {\n      setInner(next);\n      setOptimistic(null);\n      return;\n    }\n    setOptimistic(next);\n    (result as Promise<unknown>).then(\n      () => {\n        if (id > committed.current) {\n          committed.current = id;\n          setInner(next);\n        }\n        if (id === request.current) setOptimistic(null);\n      },\n      () => {\n        // A newer press owns the state now; this failure is old news.\n        if (id !== request.current) return;\n        setOptimistic(null);\n        setError(next ? \"follow\" : \"unfollow\");\n        clear.current = window.setTimeout(() => setError(null), 5000);\n      },\n    );\n  }, [following]);\n\n  const count = followers === undefined ? undefined : followers + Number(following) - Number(initial);\n  return { following, toggle, error, followers: count };\n}\n\n/* -------------------------------------------------------------------------------------------------\n * FollowButton\n * -----------------------------------------------------------------------------------------------*/\n\nexport type FollowButtonProps = Omit<React.ComponentProps<\"button\">, \"onClick\"> & {\n  following: boolean;\n  onToggle: () => void;\n  size?: \"sm\" | \"md\";\n  /** Who, for the accessible name: “Follow Leo Brandt”. */\n  name?: string;\n};\n\nexport function FollowButton({ following, onToggle, size = \"md\", name, className, onPointerEnter, onPointerLeave, ...rest }: FollowButtonProps) {\n  const reduce = useReducedMotion();\n  const [hover, setHover] = useState(false);\n  // Right after following, the pointer is still on the button. It must leave once before\n  // the label offers “Unfollow”, or the confirmation is replaced by its own undo.\n  const [armed, setArmed] = useState(true);\n  const mode = !following ? \"follow\" : hover && armed ? \"unfollow\" : \"following\";\n  const label = mode === \"follow\" ? \"Follow\" : mode === \"unfollow\" ? \"Unfollow\" : \"Following\";\n\n  return (\n    <button\n      type=\"button\"\n      aria-pressed={following}\n      aria-label={name ? `Follow ${name}` : \"Follow\"}\n      data-state={mode}\n      data-size={size}\n      onClick={() => {\n        if (!following) setArmed(false);\n        onToggle();\n      }}\n      onPointerEnter={(e) => {\n        if (e.pointerType === \"mouse\") setHover(true);\n        onPointerEnter?.(e);\n      }}\n      onPointerLeave={(e) => {\n        setHover(false);\n        setArmed(true);\n        onPointerLeave?.(e);\n      }}\n      className={cn(\n        \"relative inline-flex shrink-0 select-none items-center justify-center font-medium tracking-[-0.005em]\",\n        \"outline-none focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3\",\n        \"transition-[background-color,border-color,color,box-shadow,scale] duration-150 ease-out active:scale-[0.97] active:duration-75 motion-reduce:active:scale-100\",\n        \"disabled:pointer-events-none disabled:opacity-50\",\n        size === \"sm\" ? \"h-7 gap-1.5 rounded-md px-2.5 text-[12px]\" : \"h-8 gap-2 rounded-lg px-3 text-[12.5px]\",\n        \"border\",\n        mode === \"follow\" && \"border-transparent bg-fg text-frame hover:bg-fg/90\",\n        mode === \"following\" && \"border-line-2 bg-raised text-fg shadow-[var(--shadow)]\",\n        mode === \"unfollow\" && \"border-danger/40 bg-danger-soft text-danger\",\n        className,\n      )}\n      {...rest}\n    >\n      {/* The tick draws only to confirm a press, not when hover hands the label back. */}\n      <span className=\"relative grid size-3.5 place-items-center\">\n        <AnimatePresence initial={false}>\n          <motion.span\n            key={mode}\n            className=\"absolute inset-0 grid place-items-center\"\n            initial={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.5, rotate: mode === \"follow\" ? -45 : 0 }}\n            animate={{ opacity: 1, scale: 1, rotate: 0 }}\n            exit={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.5 }}\n            transition={reduce ? { duration: 0.12 } : spring.pop}\n          >\n            {mode === \"follow\" ? <Plus size={14} /> : mode === \"unfollow\" ? <X size={14} /> : <DrawnCheck draw={!reduce && !armed} />}\n          </motion.span>\n        </AnimatePresence>\n      </span>\n      {/* Every label shares one grid cell, so the button keeps the width of the longest. */}\n      <span className=\"grid text-left\">\n        {[\"Follow\", \"Following\", \"Unfollow\"].map((l) => (\n          <span key={l} aria-hidden className=\"invisible col-start-1 row-start-1\">\n            {l}\n          </span>\n        ))}\n        <AnimatePresence initial={false}>\n          <motion.span\n            key={label}\n            aria-hidden\n            className=\"col-start-1 row-start-1\"\n            initial={reduce ? { opacity: 0 } : { opacity: 0, y: 5, filter: \"blur(2px)\" }}\n            animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n            exit={reduce ? { opacity: 0 } : { opacity: 0, y: -5, filter: \"blur(2px)\", transition: { duration: 0.12 } }}\n            transition={{ duration: reduce ? 0.12 : 0.2, ease: ease.out }}\n          >\n            {label}\n          </motion.span>\n        </AnimatePresence>\n      </span>\n    </button>\n  );\n}\n\nfunction DrawnCheck({ draw }: { draw: boolean }) {\n  return (\n    <svg\n      width={14}\n      height={14}\n      viewBox=\"0 0 16 16\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={1.5}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      aria-hidden\n    >\n      <motion.path\n        d=\"M3.5 8.5 6.5 11.5 12.5 4.5\"\n        initial={draw ? { pathLength: 0 } : false}\n        animate={{ pathLength: 1 }}\n        transition={{ duration: 0.3, ease: ease.out, delay: 0.05 }}\n      />\n    </svg>\n  );\n}\n\n/* -------------------------------------------------------------------------------------------------\n * ProfileCard\n * -----------------------------------------------------------------------------------------------*/\n\nexport type ProfileStat = { label: string; value: number };\n\nexport type ProfileCardProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  name: string;\n  /** Without the @. */\n  handle?: string;\n  role?: string;\n  avatarSrc?: string;\n  /** Link for the name, usually the full profile. */\n  href?: string;\n  presence?: Presence;\n  /** A custom status: “In a meeting until 15:00”. */\n  statusText?: string;\n  bio?: string;\n  location?: string;\n  /** IANA zone, for the person's local time next to their location: \"Europe/Berlin\". */\n  timeZone?: string;\n  /** Shown first in the stats, and moves by one as you follow and unfollow. */\n  followers?: number;\n  stats?: ProfileStat[];\n  following?: boolean;\n  defaultFollowing?: boolean;\n  onFollowingChange?: (following: boolean) => void | Promise<unknown>;\n  /** Shows the Message button. */\n  onMessage?: () => void;\n  /** The card is you: Edit profile replaces Follow and Message. */\n  self?: boolean;\n  onEdit?: () => void;\n  /** `full` for profiles and popovers, `compact` for lists of people. */\n  variant?: \"full\" | \"compact\";\n  /** A skeleton in the card's own shape while the person loads. */\n  loading?: boolean;\n  locale?: string;\n};\n\nexport function ProfileCard({\n  name,\n  handle,\n  role,\n  avatarSrc,\n  href,\n  presence,\n  statusText,\n  bio,\n  location,\n  timeZone,\n  followers: followerCount,\n  stats = [],\n  following: followingProp,\n  defaultFollowing,\n  onFollowingChange,\n  onMessage,\n  self = false,\n  onEdit,\n  variant = \"full\",\n  loading = false,\n  locale = \"en-US\",\n  className,\n  ...rest\n}: ProfileCardProps) {\n  const follow = useFollow({ following: followingProp, defaultFollowing, onFollowingChange, followers: followerCount });\n  const reduce = useReducedMotion();\n  const compact = variant === \"compact\";\n\n  // Content that replaces a skeleton fades in over it; content that was there first just is.\n  const [hadSkeleton] = useState(loading);\n  const arrive = hadSkeleton && \"transition-opacity duration-200 ease-out starting:opacity-0\";\n\n  if (loading) return <ProfileSkeleton compact={compact} className={className} {...rest} />;\n\n  const allStats: ProfileStat[] = [...(follow.followers !== undefined ? [{ label: \"Followers\", value: follow.followers }] : []), ...stats];\n  const nameEl = href ? (\n    <a\n      href={href}\n      className=\"rounded-[3px] underline decoration-transparent underline-offset-[3px] transition-[text-decoration-color] duration-150 outline-none hover:decoration-fg-4 focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3\"\n    >\n      {name}\n    </a>\n  ) : (\n    name\n  );\n\n  const actions = self ? (\n    onEdit && (\n      <button\n        type=\"button\"\n        onClick={onEdit}\n        className={cn(\n          \"inline-flex shrink-0 items-center justify-center gap-2 border border-line-2 bg-raised font-medium text-fg shadow-[var(--shadow)] select-none\",\n          \"outline-none focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3\",\n          \"transition-[background-color,border-color,scale] duration-150 ease-out hover:border-fg-4 hover:bg-hover active:scale-[0.97] active:duration-75 motion-reduce:active:scale-100\",\n          compact ? \"h-7 rounded-md px-2.5 text-[12px]\" : \"h-8 flex-1 rounded-lg px-3 text-[12.5px]\",\n        )}\n      >\n        <Pencil size={14} />\n        Edit profile\n      </button>\n    )\n  ) : (\n    <>\n      <FollowButton\n        following={follow.following}\n        onToggle={follow.toggle}\n        name={name}\n        size={compact ? \"sm\" : \"md\"}\n        className={compact ? undefined : \"flex-1\"}\n      />\n      {onMessage && (\n        <button\n          type=\"button\"\n          onClick={onMessage}\n          aria-label={compact ? `Message ${name}` : undefined}\n          className={cn(\n            \"group relative inline-flex shrink-0 items-center justify-center gap-2 border border-line-2 bg-raised font-medium text-fg shadow-[var(--shadow)] select-none\",\n            \"outline-none focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3\",\n            \"transition-[background-color,border-color,scale] duration-150 ease-out hover:border-fg-4 hover:bg-hover active:scale-[0.97] active:duration-75 motion-reduce:active:scale-100\",\n            compact\n              ? \"size-7 rounded-md active:scale-[0.93] before:absolute before:-inset-2 before:content-[''] pointer-fine:before:hidden\"\n              : \"h-8 flex-1 rounded-lg px-3 text-[12.5px]\",\n          )}\n        >\n          <Message size={compact ? 14 : 16} className=\"transition-transform duration-200 ease-out group-hover:-translate-y-px\" />\n          {!compact && \"Message\"}\n        </button>\n      )}\n    </>\n  );\n\n  const errorLine = (\n    <div role=\"status\" aria-live=\"polite\" className=\"empty:hidden\">\n      <AnimatePresence initial={false}>\n        {follow.error && (\n          <motion.p\n            key={follow.error}\n            initial={reduce ? { opacity: 0 } : { opacity: 0, height: 0 }}\n            animate={{ opacity: 1, height: \"auto\" }}\n            exit={{ opacity: 0, height: 0, transition: { duration: 0.15 } }}\n            transition={{ duration: reduce ? 0.12 : 0.22, ease: ease.out }}\n            className=\"overflow-hidden text-[12px] text-danger\"\n          >\n            <span className=\"block pt-2\">\n              {follow.error === \"follow\" ? `Couldn’t follow ${firstName(name)}.` : `Couldn’t unfollow ${firstName(name)}.`} Try again.\n            </span>\n          </motion.p>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n\n  if (compact) {\n    return (\n      <div data-variant=\"compact\" className={cn(\"flex min-w-0 flex-col\", arrive, className)} {...rest}>\n        <div className=\"flex min-w-0 items-center gap-3\">\n          <PersonAvatar name={name} src={avatarSrc} presence={presence} px={36} />\n          <div className=\"min-w-0 flex-1\">\n            <p className=\"truncate text-[13px] leading-[18px] font-medium text-fg\">{nameEl}</p>\n            <p className=\"truncate text-[12px] leading-4 text-fg-3\">{role ?? (handle && `@${handle}`)}</p>\n          </div>\n          <div className=\"flex shrink-0 items-center gap-1.5\">{actions}</div>\n        </div>\n        {errorLine}\n      </div>\n    );\n  }\n\n  return (\n    <div\n      data-variant=\"full\"\n      className={cn(\n        \"flex w-full max-w-80 min-w-0 flex-col rounded-xl border border-line bg-raised p-4 text-fg shadow-[var(--shadow)]\",\n        arrive,\n        className,\n      )}\n      {...rest}\n    >\n      <PersonAvatar name={name} src={avatarSrc} presence={presence} px={56} />\n      <div className=\"mt-3 min-w-0\">\n        <h3 className=\"text-[15px] leading-5 font-medium tracking-[-0.015em] text-balance break-words\">{nameEl}</h3>\n        {(handle || role) && (\n          <p className=\"mt-0.5 truncate text-[12.5px] leading-[18px] text-fg-3\">{[handle && `@${handle}`, role].filter(Boolean).join(\" · \")}</p>\n        )}\n      </div>\n\n      {(presence || statusText) && (\n        <p className=\"mt-2.5 flex min-w-0 items-center gap-1.5 text-[12.5px] leading-[18px] text-fg-2\">\n          {presence && <PresenceDot presence={presence} size={8} />}\n          <span className=\"truncate\">\n            {presence && <span className=\"text-fg\">{presenceLabel[presence]}</span>}\n            {presence && statusText && <span className=\"text-fg-4\"> · </span>}\n            {statusText}\n          </span>\n        </p>\n      )}\n\n      {bio && <p className=\"mt-2.5 line-clamp-3 text-[13px] leading-[1.5] text-pretty text-fg-2\">{bio}</p>}\n\n      {(location || timeZone) && (\n        <ul className=\"mt-2.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-[12px] leading-[18px] text-fg-3\">\n          {location && (\n            <li className=\"flex min-w-0 items-center gap-1.5\">\n              <Globe size={14} className=\"shrink-0 text-fg-4\" />\n              <span className=\"truncate\">{location}</span>\n            </li>\n          )}\n          {timeZone && (\n            <li className=\"flex items-center gap-1.5\">\n              <Clock size={14} className=\"shrink-0 text-fg-4\" />\n              <LocalTime timeZone={timeZone} locale={locale} />\n            </li>\n          )}\n        </ul>\n      )}\n\n      {allStats.length > 0 && (\n        <dl className=\"mt-3.5 flex gap-5 border-t border-line pt-3\">\n          {allStats.map((s) => (\n            <div key={s.label} className=\"flex min-w-0 flex-col-reverse\">\n              <dt className=\"truncate text-[11.5px] leading-4 text-fg-3\">{s.label}</dt>\n              <dd className=\"text-[14px] leading-5 font-medium tracking-[-0.01em] text-fg tabular\">\n                <NumberFlow\n                  value={s.value}\n                  locales={locale}\n                  format={s.value >= 10000 ? { notation: \"compact\", maximumFractionDigits: 1 } : undefined}\n                  animated={!reduce}\n                  transformTiming={{ duration: 450, easing: \"cubic-bezier(0.16, 1, 0.3, 1)\" }}\n                  spinTiming={{ duration: 450, easing: \"cubic-bezier(0.16, 1, 0.3, 1)\" }}\n                />\n              </dd>\n            </div>\n          ))}\n        </dl>\n      )}\n\n      {actions && <div className={cn(\"flex gap-2\", allStats.length > 0 ? \"mt-3.5\" : \"mt-4\")}>{actions}</div>}\n      {errorLine}\n    </div>\n  );\n}\n\nconst firstName = (name: string) => name.trim().split(/\\s+/)[0] ?? name;\n\n/* -------------------------------------------------------------------------------------------------\n * Parts\n * -----------------------------------------------------------------------------------------------*/\n\n// The person's clock, ticking on the minute. Rendered after mount so the server's time zone and\n// clock never reach the markup; a same-width placeholder holds the line until then.\nfunction LocalTime({ timeZone, locale }: { timeZone: string; locale: string }) {\n  const [now, setNow] = useState<string | null>(null);\n  useEffect(() => {\n    let fmt: Intl.DateTimeFormat;\n    try {\n      fmt = new Intl.DateTimeFormat(locale, { hour: \"numeric\", minute: \"2-digit\", timeZone });\n    } catch {\n      return;\n    }\n    let timer = 0;\n    const tick = () => {\n      setNow(fmt.format(new Date()));\n      timer = window.setTimeout(tick, 60000 - (Date.now() % 60000) + 50);\n    };\n    timer = window.setTimeout(tick, 0);\n    return () => window.clearTimeout(timer);\n  }, [timeZone, locale]);\n  return (\n    <span className=\"grid tabular\">\n      <span aria-hidden className=\"invisible col-start-1 row-start-1\">\n        00:00 PM local\n      </span>\n      <span className=\"col-start-1 row-start-1 transition-opacity duration-200 ease-out\" style={{ opacity: now ? 1 : 0 }}>\n        {now ? `${now} local` : \"\"}\n      </span>\n    </span>\n  );\n}\n\nconst presenceTone: Record<Presence, string> = {\n  online: \"bg-success\",\n  away: \"bg-warning\",\n  busy: \"bg-danger\",\n  offline: \"bg-transparent shadow-[inset_0_0_0_1.5px_var(--fg-3)]\",\n};\n\n// Each presence has its own shape as well as color: busy carries a bar, offline is hollow.\nfunction PresenceDot({ presence, size }: { presence: Presence; size: number }) {\n  return (\n    <span\n      aria-hidden\n      data-presence={presence}\n      style={{ width: size, height: size }}\n      className={cn(\n        \"relative inline-grid shrink-0 place-items-center rounded-full transition-[background-color,box-shadow] duration-200 ease-out\",\n        presenceTone[presence],\n      )}\n    >\n      {presence === \"busy\" && <span className=\"h-[1.5px] w-1/2 rounded-full bg-raised\" />}\n    </span>\n  );\n}\n\nfunction initials(name: string) {\n  const words = name\n    .replace(/\\(.*?\\)/g, \" \")\n    .trim()\n    .split(/[\\s._]+/)\n    .filter(Boolean);\n  if (!words.length) return \"\";\n  const first = (w: string) => Array.from(w)[0] ?? \"\";\n  return (words.length === 1 ? first(words[0]) : first(words[0]) + first(words[words.length - 1])).toUpperCase();\n}\n\nconst tones = [\"bg-fg/[0.08] text-fg-2\", \"bg-fg/[0.12] text-fg-2\", \"bg-fg/[0.16] text-fg\", \"bg-fg/[0.2] text-fg\"] as const;\nfunction tone(key: string) {\n  let h = 0x811c9dc5;\n  for (let i = 0; i < key.length; i++) h = Math.imul(h ^ key.charCodeAt(i), 0x01000193);\n  return tones[(h >>> 0) % tones.length];\n}\n\nfunction PersonAvatar({ name, src, presence, px }: { name: string; src?: string; presence?: Presence; px: number }) {\n  const dot = px >= 48 ? 12 : 10;\n  return (\n    <span className=\"relative inline-grid shrink-0\" style={{ width: px, height: px }}>\n      <Avatar.Root className={cn(\"grid size-full place-items-center overflow-hidden rounded-full select-none\", tone(name))}>\n        <Avatar.Image\n          src={src}\n          alt=\"\"\n          width={px}\n          height={px}\n          className=\"col-start-1 row-start-1 size-full object-cover transition-opacity duration-200 ease-out data-starting-style:opacity-0\"\n        />\n        <Avatar.Fallback aria-hidden className=\"col-start-1 row-start-1 font-medium\" style={{ fontSize: Math.round(px * 0.36) }}>\n          {initials(name)}\n        </Avatar.Fallback>\n      </Avatar.Root>\n      {presence && (\n        // The ring is the card's own surface, so the dot looks cut out of the avatar.\n        <span\n          className=\"absolute right-0 bottom-0 grid place-items-center rounded-full bg-raised p-[2px]\"\n          style={{ translate: px >= 48 ? \"-1px -1px\" : \"1px 1px\" }}\n        >\n          <PresenceDot presence={presence} size={dot - 4} />\n          <span className=\"sr-only\">{presenceLabel[presence]}</span>\n        </span>\n      )}\n    </span>\n  );\n}\n\nfunction ProfileSkeleton({ compact, className, ...rest }: { compact: boolean } & React.ComponentProps<\"div\">) {\n  const fill = \"bg-fg/[0.07] animate-pulse-soft motion-reduce:animate-none\";\n  const bar = cn(fill, \"rounded-[4px]\");\n  if (compact)\n    return (\n      <div aria-busy data-variant=\"compact\" className={cn(\"flex items-center gap-3\", className)} {...rest}>\n        <span className={cn(fill, \"size-9 shrink-0 rounded-full\")} />\n        <div className=\"flex min-w-0 flex-1 flex-col\">\n          <span className=\"flex h-[18px] items-center\">\n            <span className={cn(bar, \"h-3 w-28 max-w-full\")} />\n          </span>\n          <span className=\"flex h-4 items-center\">\n            <span className={cn(bar, \"h-2.5 w-20 max-w-full\")} />\n          </span>\n        </div>\n        <span className={cn(fill, \"h-7 w-[96px] shrink-0 rounded-md\")} />\n        <span className=\"sr-only\">Loading profile</span>\n      </div>\n    );\n  return (\n    <div\n      aria-busy\n      data-variant=\"full\"\n      className={cn(\"flex w-full max-w-80 flex-col rounded-xl border border-line bg-raised p-4 shadow-[var(--shadow)]\", className)}\n      {...rest}\n    >\n      <span className={cn(fill, \"size-14 rounded-full\")} />\n      <span className=\"mt-3 flex h-5 items-center\">\n        <span className={cn(bar, \"h-3.5 w-36\")} />\n      </span>\n      <span className=\"mt-0.5 flex h-[18px] items-center\">\n        <span className={cn(bar, \"h-2.5 w-44\")} />\n      </span>\n      <span className=\"mt-2.5 flex h-[18px] items-center\">\n        <span className={cn(bar, \"h-2.5 w-full\")} />\n      </span>\n      <span className=\"flex h-[19.5px] items-center\">\n        <span className={cn(bar, \"h-2.5 w-2/3\")} />\n      </span>\n      <div className=\"mt-3.5 flex gap-5 border-t border-line pt-3\">\n        {[0, 1, 2].map((i) => (\n          <span key={i} className=\"flex flex-col gap-1.5 py-0.5\">\n            <span className={cn(bar, \"h-3.5 w-10\")} />\n            <span className={cn(bar, \"h-2.5 w-14\")} />\n          </span>\n        ))}\n      </div>\n      <div className=\"mt-3.5 flex gap-2\">\n        <span className={cn(fill, \"h-8 flex-1 rounded-lg\")} />\n        <span className={cn(fill, \"h-8 flex-1 rounded-lg\")} />\n      </div>\n      <span className=\"sr-only\">Loading profile</span>\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/profile-card"
}
