{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"profile-menu-clean","type":"registry:ui","title":"Profile Menu (Clean)","description":"Account card with the signed-in user, menu actions and profile setup progress","dependencies":[],"files":[{"path":"registry/profile-menu-clean/profile-menu-clean.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useEffect, useRef, useState, type KeyboardEvent, type ReactNode } from \"react\";\n\n/*\n * Profile Menu (Clean) – an account card with the signed-in user, menu actions and profile setup progress, in a single self-contained file.\n * Clean style: no tray – one white menu card with hairline shadows; the setup progress is its last row under a hairline.\n * The styles are injected below, so no extra files are needed.\n */\n\nexport type ProfileMenuItem = {\n  id: string;\n  label: string;\n  icon?: ReactNode;\n  /** Small pill on the right, e.g. \"New\" */\n  badge?: string;\n  onSelect?: () => void;\n};\n\nexport type ProfileSetup = {\n  /** Steps finished so far */\n  done: number;\n  total: number;\n  /** The next open step, shown as a shortcut under the bar */\n  next?: string;\n  onContinue?: () => void;\n};\n\nexport type ProfileMenuProps = {\n  name: string;\n  email: string;\n  /** Replaces the default avatar */\n  avatar?: ReactNode;\n  items?: ProfileMenuItem[];\n  /** Profile setup progress under the actions – leave empty to hide */\n  setup?: ProfileSetup;\n  className?: string;\n};\n\n// Counts get separators; anything that isn't a finite number reads as 0\nconst count = new Intl.NumberFormat(\"en\");\nconst finite = (n: unknown) => (typeof n === \"number\" && Number.isFinite(n) ? n : 0);\n\nexport function ProfileMenu({\n  name,\n  email,\n  avatar,\n  items = defaultItems,\n  setup,\n  className,\n}: ProfileMenuProps) {\n  const listRef = useRef<HTMLDivElement>(null);\n  // Gaps in the list are skipped, an image that fails to load falls back to the default avatar\n  const list = (Array.isArray(items) ? items : []).filter((item): item is ProfileMenuItem => !!item && typeof item === \"object\");\n  const [broken, setBroken] = useState(false);\n  const shownName = typeof name === \"string\" ? name.trim() : \"\";\n  // The progress bar fills up once the card is on screen\n  const [filled, setFilled] = useState(false);\n  const rootRef = useRef<HTMLElement>(null);\n  useEffect(() => {\n    const el = rootRef.current;\n    if (!el) return;\n    const io = new IntersectionObserver(([e]) => e.isIntersecting && (setFilled(true), io.disconnect()), {\n      threshold: 0.3,\n    });\n    io.observe(el);\n    return () => io.disconnect();\n  }, []);\n\n  // Arrow keys move between the actions, like in a native menu\n  const onKey = (e: KeyboardEvent) => {\n    const buttons = [...(listRef.current?.querySelectorAll<HTMLButtonElement>(\"button\") ?? [])];\n    const i = buttons.indexOf(document.activeElement as HTMLButtonElement);\n    const next =\n      e.key === \"ArrowDown\" ? (i + 1) % buttons.length\n      : e.key === \"ArrowUp\" ? (i - 1 + buttons.length) % buttons.length\n      : e.key === \"Home\" ? 0\n      : e.key === \"End\" ? buttons.length - 1\n      : null;\n    if (next === null) return;\n    e.preventDefault();\n    buttons[next]?.focus();\n  };\n\n  return (\n    <>\n      <style href=\"ui-profile-menu-clean\" precedence=\"default\">\n        {css}\n      </style>\n      <section ref={rootRef} className={[\"ui-pmc\", className].filter(Boolean).join(\" \")} data-filled={filled || undefined}>\n        <div className=\"ui-pmc__card\">\n        <header className=\"ui-pmc__user\">\n          <span className=\"ui-pmc__avatar\" onErrorCapture={() => setBroken(true)}>\n            {avatar && !broken ? avatar : <DefaultAvatar />}\n          </span>\n          <span className=\"ui-pmc__who\">\n            {shownName && (\n              <span className=\"ui-pmc__name\" dir=\"auto\" title={shownName}>\n                {shownName}\n              </span>\n            )}\n            <span className=\"ui-pmc__email\" dir=\"auto\" title={email}>\n              {email}\n            </span>\n          </span>\n        </header>\n\n        <div ref={listRef} className=\"ui-pmc__items\" role=\"menu\" aria-label=\"Account\" onKeyDown={onKey}>\n          {list.map((item, i) => (\n            <button key={`${item.id}-${i}`} type=\"button\" role=\"menuitem\" className=\"ui-pmc__item\" onClick={item.onSelect}>\n              <span className=\"ui-pmc__icon\">{item.icon}</span>\n              <span className=\"ui-pmc__label\" dir=\"auto\" title={item.label}>\n                {item.label}\n              </span>\n              {item.badge && (\n                <span className=\"ui-pmc__badge\" dir=\"auto\" title={item.badge}>\n                  {item.badge}\n                </span>\n              )}\n            </button>\n          ))}\n        </div>\n\n        {setup && <SetupProgress {...setup} />}\n        </div>\n      </section>\n    </>\n  );\n}\n\n/** Compact setup progress as the last row of the card: count, bar and the next step on one line */\nfunction SetupProgress({ done: rawDone, total: rawTotal, next, onContinue }: ProfileSetup) {\n  const total = Math.max(0, finite(rawTotal));\n  const done = Math.min(total, Math.max(0, finite(rawDone)));\n  const ratio = total > 0 ? done / total : 0;\n  const percent = Math.round(ratio * 100);\n  const open = next && done < total;\n  return (\n    <div className=\"ui-pmc__setup\">\n      <div className=\"ui-pmc__setup-head\">\n        <span className=\"ui-pmc__setup-title\">Profile setup</span>\n        <span className=\"ui-pmc__setup-count\">\n          {count.format(done)}/{count.format(total)}\n        </span>\n      </div>\n      <span\n        className=\"ui-pmc__track\"\n        role=\"progressbar\"\n        aria-label=\"Profile setup\"\n        aria-valuemin={0}\n        aria-valuemax={100}\n        aria-valuenow={percent}\n      >\n        <span className=\"ui-pmc__fill\" style={{ transform: `scaleX(${ratio})` }} />\n      </span>\n      {open && (\n        <button type=\"button\" className=\"ui-pmc__next\" onClick={onContinue}>\n          <span className=\"ui-pmc__next-label\" dir=\"auto\" title={next}>\n            <span className=\"ui-pmc__next-hint\">Next:</span> {next}\n          </span>\n          <ArrowIcon />\n        </button>\n      )}\n    </div>\n  );\n}\n\n/* ---------- Icons ---------- */\n\nconst Svg = ({ children }: { children: ReactNode }) => (\n  <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.1\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n    {children}\n  </svg>\n);\n\nexport const SettingsIcon = () => (\n  <Svg>\n    <path d=\"M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z\" />\n    <circle cx=\"12\" cy=\"12\" r=\"3\" />\n  </Svg>\n);\n\nexport const LanguageIcon = () => (\n  <Svg>\n    <path d=\"m5 8 6 6M4 14l6-6 2-3M2 5h12M7 2h1M22 22l-5-10-5 10M14 18h6\" />\n  </Svg>\n);\n\nexport const DocsIcon = () => (\n  <Svg>\n    <path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\" />\n    <path d=\"M14 2v4a2 2 0 0 0 2 2h4M16 13H8M16 17H8M10 9H8\" />\n  </Svg>\n);\n\nexport const AppsIcon = () => (\n  <Svg>\n    <rect x=\"3\" y=\"3\" width=\"7\" height=\"7\" rx=\"1.5\" />\n    <rect x=\"3\" y=\"14\" width=\"7\" height=\"7\" rx=\"1.5\" />\n    <rect x=\"14\" y=\"14\" width=\"7\" height=\"7\" rx=\"1.5\" />\n    <path d=\"M17.5 3v7M14 6.5h7\" />\n  </Svg>\n);\n\nexport const LogoutIcon = () => (\n  <Svg>\n    <path d=\"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9\" />\n  </Svg>\n);\n\n/** Default avatar: a person icon centered on a soft blue disc */\nconst DefaultAvatar = () => (\n  <svg viewBox=\"0 0 40 40\" aria-hidden=\"true\">\n    <circle cx=\"20\" cy=\"20\" r=\"20\" fill=\"currentColor\" opacity=\"0.1\" />\n    <circle cx=\"20\" cy=\"16\" r=\"4.75\" fill=\"currentColor\" />\n    <path d=\"M11.5 28.25c.9-4 4.3-6.75 8.5-6.75s7.6 2.75 8.5 6.75c.15.7-.4 1.25-1.1 1.25H12.6c-.7 0-1.25-.55-1.1-1.25Z\" fill=\"currentColor\" />\n  </svg>\n);\n\nconst ArrowIcon = () => (\n  <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.1\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n    <path d=\"M5 12h14M13 6l6 6-6 6\" />\n  </svg>\n);\n\nconst defaultItems: ProfileMenuItem[] = [\n  { id: \"settings\", label: \"Settings\", icon: <SettingsIcon /> },\n  { id: \"docs\", label: \"Docs\", icon: <DocsIcon /> },\n  { id: \"apps\", label: \"Apps\", icon: <AppsIcon />, badge: \"New\" },\n  { id: \"logout\", label: \"Logout\", icon: <LogoutIcon /> },\n];\n\n/* ---------- Styles ---------- */\n\nconst css = /* css */ `\n/* Colors and motion as variables – override them on .ui-pmc */\n.ui-pmc {\n  --ui-pmc-card: var(--aui-overlay, #ffffff);\n  --ui-pmc-strong: var(--aui-ink, #000000);\n  --ui-pmc-ink: var(--aui-ink, rgba(0, 0, 0, 0.88));\n  --ui-pmc-text: var(--aui-muted, rgba(0, 0, 0, 0.61));\n  --ui-pmc-muted: var(--aui-muted, rgba(0, 0, 0, 0.45));\n  --ui-pmc-faint: var(--aui-placeholder, rgba(0, 0, 0, 0.27));\n  --ui-pmc-icon: var(--aui-muted, rgba(0, 0, 0, 0.447));\n  --ui-pmc-line: var(--aui-border, rgba(0, 0, 0, 0.06));\n  --ui-pmc-wash: var(--aui-subtle, rgba(0, 0, 0, 0.03));\n  --ui-pmc-hover: var(--aui-subtle, rgba(0, 0, 0, 0.06));\n  --ui-pmc-track: rgba(0, 0, 0, 0.06);\n  --ui-pmc-accent: rgba(0, 96, 226, 0.765);\n  --ui-pmc-focus: var(--aui-focus, rgba(0, 96, 226, 0.7));\n  --ui-pmc-badge: #e12a1d;\n  --ui-pmc-badge-wash: color-mix(in srgb, var(--aui-red, #f04438) 8%, transparent);\n  /* Strokes are shadows: a 0.5px hairline plus a soft lift, like a popover */\n  --ui-pmc-shadow: var(--aui-shadow-popover, 0 0 0 0.5px rgba(0, 0, 0, 0.08), 0 12px 28px -10px rgba(0, 0, 0, 0.16), 0 1px 2px 0 rgba(0, 0, 0, 0.05));\n  --ui-pmc-ease: var(--aui-ease, cubic-bezier(0.22, 1, 0.36, 1));\n  box-sizing: border-box;\n  container-type: inline-size;\n  width: 100%;\n  max-width: 260px;\n  font-family: var(--aui-font, \"Inter\", \"InterVariable\", ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif);\n  font-size: 13px;\n  line-height: 20px;\n  font-weight: 400;\n  color: var(--ui-pmc-ink);\n  -webkit-font-smoothing: antialiased;\n}\n.ui-pmc *, .ui-pmc *::before, .ui-pmc *::after { box-sizing: border-box; }\n:where(.ui-pmc) button { margin: 0; padding: 0; border: 0; background: none; font: inherit; color: inherit; text-align: left; cursor: pointer; }\n:where(.ui-pmc) svg { display: block; flex: none; }\n\n/* One white menu card – the edge is a shadow, not a border */\n.ui-pmc__card {\n  padding: 4px;\n  border-radius: var(--aui-radius-lg, 10px);\n  background: var(--ui-pmc-card);\n  box-shadow: var(--ui-pmc-shadow);\n}\n\n/* The signed-in user, separated from the actions by a hairline */\n.ui-pmc__user {\n  display: flex;\n  align-items: center;\n  gap: 10px;\n  margin: 0 -4px 4px;\n  padding: 8px 12px 10px;\n  border-bottom: 0.5px solid var(--ui-pmc-line);\n}\n.ui-pmc__avatar {\n  flex: none;\n  width: 32px;\n  height: 32px;\n  overflow: hidden;\n  border-radius: 50%;\n  color: var(--ui-pmc-accent);\n}\n.ui-pmc__avatar > * { display: block; width: 100%; height: 100%; object-fit: cover; }\n.ui-pmc__who { display: grid; min-width: 0; }\n.ui-pmc__name {\n  overflow: hidden;\n  font-weight: 500;\n  color: var(--ui-pmc-strong);\n  white-space: nowrap;\n  text-overflow: ellipsis;\n}\n.ui-pmc__email {\n  overflow: hidden;\n  font-size: 12px;\n  line-height: 16px;\n  color: var(--ui-pmc-muted);\n  white-space: nowrap;\n  text-overflow: ellipsis;\n}\n\n/* Actions: quiet rows, a subtle fill on hover that fades out again */\n/* The column may shrink, so long labels shorten instead of widening the card; a long list scrolls */\n.ui-pmc__items { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0; max-height: 320px; overflow-y: auto; overscroll-behavior: contain; }\n.ui-pmc__item {\n  display: flex;\n  align-items: center;\n  gap: 10px;\n  width: 100%;\n  height: 32px;\n  padding: 0 8px;\n  border-radius: calc(var(--aui-radius-sm, 5px) + 1px);\n  color: var(--ui-pmc-ink);\n  transition: background-color 140ms ease;\n}\n.ui-pmc__item:hover { background: var(--ui-pmc-hover); transition-duration: 60ms; }\n.ui-pmc__item:focus-visible { background: var(--ui-pmc-hover); outline: none; }\n.ui-pmc__item:active { background: rgba(0, 0, 0, 0.08); }\n\n.ui-pmc__icon {\n  display: grid;\n  place-items: center;\n  flex: none;\n  width: 16px;\n  height: 16px;\n  color: var(--ui-pmc-icon);\n  transition: color 140ms ease;\n}\n.ui-pmc__icon svg { width: 16px; height: 16px; }\n.ui-pmc__icon svg * { vector-effect: non-scaling-stroke; }\n.ui-pmc__item:hover .ui-pmc__icon,\n.ui-pmc__item:focus-visible .ui-pmc__icon { color: var(--ui-pmc-ink); }\n\n.ui-pmc__label {\n  flex: 1;\n  min-width: 0;\n  overflow: hidden;\n  white-space: nowrap;\n  text-overflow: ellipsis;\n}\n.ui-pmc__badge {\n  flex: none;\n  max-width: 50%;\n  overflow: hidden;\n  white-space: nowrap;\n  text-overflow: ellipsis;\n  padding: 0 6px;\n  border-radius: var(--aui-radius-sm, 5px);\n  background: var(--ui-pmc-badge-wash);\n  font-size: 11px;\n  font-weight: 500;\n  line-height: 18px;\n  color: var(--ui-pmc-badge);\n}\n\n/* Profile setup: the last row of the card, under a hairline */\n.ui-pmc__setup {\n  margin: 4px -4px -4px;\n  padding: 10px 12px 12px;\n  border-top: 0.5px solid var(--ui-pmc-line);\n}\n.ui-pmc__setup:has(.ui-pmc__next) { padding-bottom: 6px; }\n.ui-pmc__setup-head {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  gap: 8px;\n  font-size: 12px;\n  line-height: 16px;\n}\n.ui-pmc__setup-title { color: var(--ui-pmc-text); font-weight: 500; }\n.ui-pmc__setup-count { font-variant-numeric: tabular-nums; color: var(--ui-pmc-muted); }\n.ui-pmc__track {\n  display: block;\n  height: 4px;\n  margin-top: 8px;\n  overflow: hidden;\n  border-radius: 2px;\n  background: var(--ui-pmc-track);\n}\n.ui-pmc__fill {\n  display: block;\n  height: 100%;\n  border-radius: inherit;\n  background: var(--ui-pmc-accent);\n  transform-origin: left center;\n  scale: 0 1;\n  transition: scale 900ms var(--ui-pmc-ease) 150ms;\n}\n.ui-pmc[data-filled] .ui-pmc__fill { scale: 1 1; }\n.ui-pmc__next {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  width: calc(100% + 16px);\n  height: 28px;\n  margin: 6px -8px 0;\n  padding: 0 8px;\n  border-radius: calc(var(--aui-radius-sm, 5px) + 1px);\n  color: var(--ui-pmc-ink);\n  transition: background-color 140ms ease;\n}\n.ui-pmc__next:hover { background: var(--ui-pmc-wash); }\n.ui-pmc__next:focus-visible { outline: 2px solid var(--ui-pmc-focus); outline-offset: 2px; }\n.ui-pmc__next-label {\n  flex: 1;\n  min-width: 0;\n  overflow: hidden;\n  white-space: nowrap;\n  text-overflow: ellipsis;\n}\n.ui-pmc__next-hint { color: var(--ui-pmc-muted); }\n.ui-pmc__next svg {\n  width: 16px;\n  height: 16px;\n  color: var(--ui-pmc-icon);\n  transition: transform 200ms var(--ui-pmc-ease), color 140ms ease;\n}\n.ui-pmc__next svg * { vector-effect: non-scaling-stroke; }\n.ui-pmc__next:hover svg { transform: translateX(2px); color: var(--ui-pmc-ink); }\n\n/* Touch screens: taller rows */\n@media (hover: none) and (pointer: coarse) {\n  .ui-pmc__item { height: 40px; }\n  .ui-pmc__next { height: 36px; }\n}\n@media (prefers-reduced-motion: reduce) {\n  .ui-pmc__item, .ui-pmc__icon, .ui-pmc__fill, .ui-pmc__next, .ui-pmc__next svg { transition: none; }\n}\n\n/* Dark mode: follows a .dark class or data-theme=\"dark\" on any ancestor, e.g. <html> */\n:where(.dark, [data-theme=\"dark\"]) .ui-pmc {\n  --ui-pmc-card: var(--aui-overlay, #1c1c1e);\n  --ui-pmc-strong: var(--aui-ink, #ffffff);\n  --ui-pmc-ink: var(--aui-ink, rgba(255, 255, 255, 0.9));\n  --ui-pmc-text: var(--aui-muted, rgba(255, 255, 255, 0.62));\n  --ui-pmc-muted: var(--aui-muted, rgba(255, 255, 255, 0.46));\n  --ui-pmc-faint: var(--aui-placeholder, rgba(255, 255, 255, 0.3));\n  --ui-pmc-icon: var(--aui-muted, rgba(255, 255, 255, 0.46));\n  --ui-pmc-line: var(--aui-border, rgba(255, 255, 255, 0.08));\n  --ui-pmc-wash: var(--aui-subtle, rgba(255, 255, 255, 0.04));\n  --ui-pmc-hover: var(--aui-subtle, rgba(255, 255, 255, 0.08));\n  --ui-pmc-track: rgba(255, 255, 255, 0.1);\n  --ui-pmc-accent: var(--aui-blue, #4fb0fe);\n  --ui-pmc-focus: var(--aui-focus, rgba(77, 141, 255, 0.8));\n  --ui-pmc-badge: var(--aui-danger, #ff8a7a);\n  --ui-pmc-badge-wash: color-mix(in srgb, var(--aui-red, #ff6b5f) 14%, transparent);\n  --ui-pmc-shadow: var(--aui-shadow-popover, 0 0 0 0.5px rgba(255, 255, 255, 0.1), 0 1px 2px rgba(0, 0, 0, 0.4));\n}\n:where(.dark, [data-theme=\"dark\"]) .ui-pmc__item:active { background: rgba(255, 255, 255, 0.1); }\n`;\n"}]}