{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "account-card",
  "type": "registry:component",
  "title": "Account Card",
  "description": "The card behind your avatar: who you're signed in as, how far your profile has come and the one step to do next, plus your menu and theme.",
  "categories": [
    "component",
    "Workspace"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://coss.com/ui/r/kbd.json",
    "https://coss.com/ui/r/segmented-control.json"
  ],
  "files": [
    {
      "path": "registry/devl/account-card.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport {\n  CheckIcon,\n  ChevronRightIcon,\n  LogOutIcon,\n  type LucideIcon,\n  MonitorIcon,\n  MoonIcon,\n  SunIcon,\n} from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { type KeyboardEvent, useEffect, useId, useRef, useState } from \"react\";\nimport { Kbd } from \"@/components/ui/kbd\";\nimport { SegmentedControl, SegmentedControlItem } from \"@/components/ui/segmented-control\";\nimport { cn } from \"@/lib/utils\";\n\nexport type ThemeChoice = \"light\" | \"dark\" | \"system\";\n\nexport interface AccountCardItem {\n  id: string;\n  label: string;\n  icon: LucideIcon;\n  shortcut?: string;\n  meta?: string;\n}\n\nexport interface AccountCardStep {\n  id: string;\n  label: string;\n  done?: boolean;\n}\n\nexport interface AccountCardProps {\n  user: { name: string; email: string; initials: string; plan?: string };\n  items: AccountCardItem[];\n  steps?: AccountCardStep[];\n  theme?: ThemeChoice;\n  onThemeChange?: (theme: ThemeChoice) => void;\n  onSelect?: (id: string) => void;\n  onSignOut?: () => void;\n  className?: string;\n}\n\nconst spring = { type: \"spring\", stiffness: 420, damping: 34 } as const;\n\nfunction Rolling({ value }: { value: number }) {\n  const reduce = useReducedMotion();\n  const prev = useRef(value);\n  const dir = value >= prev.current ? 1 : -1;\n  useEffect(() => {\n    prev.current = value;\n  }, [value]);\n  return (\n    <span className=\"relative inline-flex overflow-hidden tabular-nums\">\n      <AnimatePresence initial={false} mode=\"popLayout\">\n        <motion.span\n          animate={{ y: 0, opacity: 1 }}\n          exit={reduce ? { opacity: 0 } : { y: `${-dir * 100}%`, opacity: 0 }}\n          initial={reduce ? { opacity: 0 } : { y: `${dir * 100}%`, opacity: 0 }}\n          key={value}\n          transition={spring}\n        >\n          {value}\n        </motion.span>\n      </AnimatePresence>\n    </span>\n  );\n}\n\nconst THEMES: { value: ThemeChoice; icon: LucideIcon; label: string }[] = [\n  { value: \"light\", icon: SunIcon, label: \"Light\" },\n  { value: \"dark\", icon: MoonIcon, label: \"Dark\" },\n  { value: \"system\", icon: MonitorIcon, label: \"System\" },\n];\n\nfunction ThemeSwitch({ value, onChange }: { value: ThemeChoice; onChange: (t: ThemeChoice) => void }) {\n  const refs = useRef<(HTMLButtonElement | null)[]>([]);\n  const index = THEMES.findIndex((t) => t.value === value);\n\n  const onKeyDown = (e: KeyboardEvent) => {\n    if (e.key !== \"ArrowRight\" && e.key !== \"ArrowLeft\") return;\n    e.preventDefault();\n    const next = (index + (e.key === \"ArrowRight\" ? 1 : -1) + THEMES.length) % THEMES.length;\n    onChange(THEMES[next].value);\n    refs.current[next]?.focus();\n  };\n\n  return (\n    <SegmentedControl aria-label=\"Theme\" onKeyDown={onKeyDown} role=\"radiogroup\">\n      {THEMES.map((t, i) => {\n        const active = t.value === value;\n        const Icon = t.icon;\n        return (\n          <SegmentedControlItem\n            aria-checked={active}\n            aria-label={t.label}\n            className=\"w-8 px-0 text-muted-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n            data-checked={active ? \"\" : undefined}\n            key={t.value}\n            onClick={() => onChange(t.value)}\n            ref={(el: HTMLButtonElement | null) => {\n              refs.current[i] = el;\n            }}\n            role=\"radio\"\n            tabIndex={active ? 0 : -1}\n            title={t.label}\n          >\n            <Icon className=\"size-3.5\" />\n          </SegmentedControlItem>\n        );\n      })}\n    </SegmentedControl>\n  );\n}\n\nexport function AccountCard({\n  user,\n  items,\n  steps: initialSteps = [],\n  theme: themeProp,\n  onThemeChange,\n  onSelect,\n  onSignOut,\n  className,\n}: AccountCardProps) {\n  const reduce = useReducedMotion();\n  const hoverId = useId();\n  const [steps, setSteps] = useState(initialSteps);\n  const [themeState, setThemeState] = useState<ThemeChoice>(themeProp ?? \"system\");\n  const [hover, setHover] = useState<string | null>(null);\n  const theme = themeProp ?? themeState;\n\n  const doneCount = steps.filter((s) => s.done).length;\n  const pct = steps.length ? Math.round((doneCount / steps.length) * 100) : 100;\n  const next = steps.find((s) => !s.done);\n\n  const r = 25;\n  const c = 2 * Math.PI * r;\n\n  const setTheme = (t: ThemeChoice) => {\n    setThemeState(t);\n    onThemeChange?.(t);\n  };\n\n  return (\n    <section\n      aria-label=\"Account\"\n      className={cn(\"w-full max-w-[320px] rounded-2xl border bg-card text-card-foreground shadow-xs/5\", className)}\n    >\n      <div className=\"flex items-center gap-3 p-4\">\n        <div className=\"relative size-14 shrink-0\">\n          <svg aria-hidden className=\"-rotate-90 absolute inset-0 size-full\" viewBox=\"0 0 56 56\">\n            <circle className=\"stroke-muted\" cx=\"28\" cy=\"28\" fill=\"none\" r={r} strokeWidth=\"2.5\" />\n            <motion.circle\n              animate={{ strokeDashoffset: c * (1 - pct / 100) }}\n              className={pct === 100 ? \"stroke-success\" : \"stroke-primary\"}\n              cx=\"28\"\n              cy=\"28\"\n              fill=\"none\"\n              initial={false}\n              r={r}\n              strokeDasharray={c}\n              strokeLinecap=\"round\"\n              strokeWidth=\"2.5\"\n              transition={reduce ? { duration: 0 } : { type: \"spring\", stiffness: 110, damping: 18 }}\n            />\n          </svg>\n          <span className=\"absolute inset-[6px] flex items-center justify-center rounded-full bg-info/14 font-medium text-info-foreground text-sm\">\n            {user.initials}\n          </span>\n          <AnimatePresence>\n            {pct === 100 && (\n              <motion.span\n                animate={{ scale: 1, opacity: 1 }}\n                className=\"absolute -right-0.5 -bottom-0.5 flex size-5 items-center justify-center rounded-full border-2 border-card bg-success text-white\"\n                exit={{ scale: 0.6, opacity: 0, transition: { duration: 0.12 } }}\n                initial={reduce ? { opacity: 0 } : { scale: 0.6, opacity: 0 }}\n                transition={{ type: \"spring\", stiffness: 520, damping: 24, delay: reduce ? 0 : 0.25 }}\n              >\n                <CheckIcon className=\"size-2.5\" strokeWidth={3.5} />\n              </motion.span>\n            )}\n          </AnimatePresence>\n        </div>\n        <div className=\"min-w-0 flex-1\">\n          <div className=\"flex items-center gap-2\">\n            <p className=\"truncate font-medium text-sm\">{user.name}</p>\n            {user.plan && (\n              <span className=\"shrink-0 rounded-full bg-primary px-1.5 py-px font-medium text-[10px] text-primary-foreground uppercase tracking-wide\">\n                {user.plan}\n              </span>\n            )}\n          </div>\n          <p className=\"truncate text-muted-foreground text-xs\">{user.email}</p>\n        </div>\n      </div>\n\n      {steps.length > 0 && (\n        <div className=\"mx-3 mb-2 rounded-xl bg-muted/50 p-3\">\n          <div className=\"flex items-center justify-between text-xs\">\n            <span className=\"text-muted-foreground\">Profile setup</span>\n            <span className=\"font-medium tabular-nums\">\n              <Rolling value={pct} />%\n            </span>\n          </div>\n          <div className=\"mt-1 h-8\">\n            <AnimatePresence initial={false} mode=\"popLayout\">\n              {next ? (\n                <motion.button\n                  animate={{ opacity: 1, y: 0 }}\n                  className=\"group/next -mx-1.5 flex h-8 w-[calc(100%+--spacing(3))] cursor-pointer items-center gap-2 rounded-md px-1.5 text-left text-xs outline-none transition-[background-color,scale] duration-150 ease-out hover:bg-background/60 focus-visible:ring-2 focus-visible:ring-ring active:scale-[0.98] dark:hover:bg-input/40\"\n                  exit={{ opacity: 0, y: -10 }}\n                  initial={reduce ? false : { opacity: 0, y: 10 }}\n                  key={next.id}\n                  onClick={() => setSteps((s) => s.map((x) => (x.id === next.id ? { ...x, done: true } : x)))}\n                  transition={spring}\n                  type=\"button\"\n                >\n                  <span className=\"size-3.5 shrink-0 rounded-full border-[1.5px] border-dashed border-muted-foreground/60 transition-colors group-hover/next:border-solid group-hover/next:border-foreground\" />\n                  <span className=\"flex-1 truncate\">\n                    Next: <span className=\"font-medium\">{next.label}</span>\n                  </span>\n                  <ChevronRightIcon className=\"size-3.5 text-muted-foreground transition-transform group-hover/next:translate-x-0.5\" />\n                </motion.button>\n              ) : (\n                <motion.p\n                  animate={{ opacity: 1, y: 0 }}\n                  className=\"flex h-8 items-center gap-2 text-success-foreground text-xs\"\n                  initial={reduce ? false : { opacity: 0, y: 10 }}\n                  key=\"done\"\n                  transition={spring}\n                >\n                  <CheckIcon className=\"size-3.5\" /> All set – your profile is complete\n                </motion.p>\n              )}\n            </AnimatePresence>\n          </div>\n        </div>\n      )}\n\n      <nav aria-label=\"Account menu\" className=\"border-t p-1.5\" onMouseLeave={() => setHover(null)}>\n        {items.map((item) => {\n          const Icon = item.icon;\n          return (\n            <button\n              className=\"relative flex h-9 w-full cursor-pointer items-center gap-2.5 rounded-lg px-2.5 text-left text-sm outline-none\"\n              key={item.id}\n              onBlur={() => setHover((h) => (h === item.id ? null : h))}\n              onClick={() => onSelect?.(item.id)}\n              onFocus={() => setHover(item.id)}\n              onMouseEnter={() => setHover(item.id)}\n              type=\"button\"\n            >\n              {hover === item.id && (\n                <motion.span\n                  className=\"absolute inset-0 -z-0 rounded-lg bg-accent\"\n                  layoutId={`${hoverId}-hover`}\n                  transition={reduce ? { duration: 0 } : { type: \"spring\", stiffness: 500, damping: 38 }}\n                />\n              )}\n              <Icon className=\"relative size-4 text-muted-foreground\" />\n              <span className=\"relative flex-1\">{item.label}</span>\n              {item.meta && <span className=\"relative text-muted-foreground text-xs\">{item.meta}</span>}\n              {item.shortcut && <Kbd className=\"relative\">{item.shortcut}</Kbd>}\n            </button>\n          );\n        })}\n      </nav>\n\n      <div className=\"flex items-center justify-between border-t py-2 ps-4 pe-3\">\n        <span className=\"text-sm\">Theme</span>\n        <ThemeSwitch onChange={setTheme} value={theme} />\n      </div>\n\n      <div className=\"border-t p-1.5\">\n        <button\n          className=\"group/out flex h-9 w-full cursor-pointer items-center gap-2.5 rounded-lg px-2.5 text-left text-sm outline-none transition-colors hover:bg-destructive/8 hover:text-destructive-foreground focus-visible:bg-destructive/8 focus-visible:text-destructive-foreground\"\n          onClick={onSignOut}\n          type=\"button\"\n        >\n          <LogOutIcon className=\"size-4 text-muted-foreground transition-colors group-hover/out:text-destructive-foreground\" />\n          Sign out\n        </button>\n      </div>\n    </section>\n  );\n}\n"
    }
  ]
}