{"version":1,"id":"profile-dropdown","framework":"next","kind":"block","files":[{"path":"src/blocks/gfa/profile-dropdown.tsx","content":"\"use client\";\n\nimport {\n  useEffect,\n  useId,\n  useRef,\n  useState,\n  type ReactNode,\n} from \"react\";\nimport {\n  AnimatePresence,\n  motion,\n  useReducedMotion,\n} from \"motion/react\";\nimport {\n  Building2,\n  CreditCard,\n  LogOut,\n  Settings,\n  User,\n  UserPlus,\n  Users,\n} from \"lucide-react\";\nimport {\n  AvatarFallback,\n  AvatarImage,\n  LiquidMetalAvatar,\n} from \"@/blocks/gfa/liquid-metal-avatar\";\nimport { SPRING_PANEL, SPRING_PRESS } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport type ProfileDropdownItem = {\n  id: string;\n  label: string;\n  icon?: ReactNode;\n  onSelect?: () => void;\n};\n\nexport type ProfileDropdownProps = {\n  name?: string;\n  email?: string;\n  handle?: string;\n  avatarSrc?: string;\n  avatarAlt?: string;\n  accountItems?: ProfileDropdownItem[];\n  workspaceItems?: ProfileDropdownItem[];\n  onSignOut?: () => void;\n  /** Gallery: start open, ignore outside click */\n  preview?: boolean;\n  className?: string;\n};\n\nconst APPLE_SANS =\n  \"font-[system-ui,-apple-system,BlinkMacSystemFont,'SF_Pro_Text','SF_Pro_Display',sans-serif]\";\n\nconst DEFAULT_ACCOUNT: ProfileDropdownItem[] = [\n  {\n    id: \"profile\",\n    label: \"Profile\",\n    icon: <User className=\"size-4\" strokeWidth={1.75} />,\n  },\n  {\n    id: \"settings\",\n    label: \"Settings\",\n    icon: <Settings className=\"size-4\" strokeWidth={1.75} />,\n  },\n  {\n    id: \"billing\",\n    label: \"Billing\",\n    icon: <CreditCard className=\"size-4\" strokeWidth={1.75} />,\n  },\n];\n\nconst DEFAULT_WORKSPACE: ProfileDropdownItem[] = [\n  {\n    id: \"switch\",\n    label: \"Switch workspace\",\n    icon: <Building2 className=\"size-4\" strokeWidth={1.75} />,\n  },\n  {\n    id: \"invite\",\n    label: \"Invite members\",\n    icon: <UserPlus className=\"size-4\" strokeWidth={1.75} />,\n  },\n  {\n    id: \"workspace-settings\",\n    label: \"Workspace settings\",\n    icon: <Users className=\"size-4\" strokeWidth={1.75} />,\n  },\n];\n\nfunction initialsFromName(name: string) {\n  const parts = name.trim().split(/\\s+/).filter(Boolean);\n  if (parts.length >= 2) {\n    return `${parts[0]![0] ?? \"\"}${parts[1]![0] ?? \"\"}`.toUpperCase();\n  }\n  return name.slice(0, 2).toUpperCase() || \"?\";\n}\n\nexport function ProfileDropdown({\n  name = \"Gökhan\",\n  email = \"gokhan@gfa.app\",\n  handle = \"@gokhanf4\",\n  avatarSrc = \"/avatars/gokhan.png\",\n  avatarAlt,\n  accountItems = DEFAULT_ACCOUNT,\n  workspaceItems = DEFAULT_WORKSPACE,\n  onSignOut,\n  preview = false,\n  className,\n}: ProfileDropdownProps) {\n  const reduceMotion = useReducedMotion();\n  const rootRef = useRef<HTMLDivElement>(null);\n  const triggerRef = useRef<HTMLButtonElement>(null);\n  const menuId = useId();\n  const [open, setOpen] = useState(preview);\n  const initials = initialsFromName(name);\n\n  useEffect(() => {\n    if (preview || !open) return;\n    const onKey = (e: KeyboardEvent) => {\n      if (e.key === \"Escape\") {\n        setOpen(false);\n        triggerRef.current?.focus();\n      }\n    };\n    const onPointer = (e: MouseEvent | TouchEvent) => {\n      const el = rootRef.current;\n      if (!el) return;\n      const target = e.target as Node | null;\n      if (target && !el.contains(target)) setOpen(false);\n    };\n    document.addEventListener(\"keydown\", onKey);\n    document.addEventListener(\"mousedown\", onPointer);\n    document.addEventListener(\"touchstart\", onPointer);\n    return () => {\n      document.removeEventListener(\"keydown\", onKey);\n      document.removeEventListener(\"mousedown\", onPointer);\n      document.removeEventListener(\"touchstart\", onPointer);\n    };\n  }, [open, preview]);\n\n  const runItem = (item: ProfileDropdownItem) => {\n    item.onSelect?.();\n    if (!preview) setOpen(false);\n  };\n\n  return (\n    <div\n      ref={rootRef}\n      data-gfa-profile-dropdown\n      className={cn(APPLE_SANS, \"relative inline-flex\", className)}\n    >\n      <motion.button\n        ref={triggerRef}\n        type=\"button\"\n        aria-haspopup=\"menu\"\n        aria-expanded={open}\n        aria-controls={menuId}\n        whileTap={reduceMotion || preview ? undefined : { scale: 0.98 }}\n        transition={SPRING_PRESS}\n        onClick={() => {\n          if (preview) return;\n          setOpen((v) => !v);\n        }}\n        className={cn(\n          \"inline-flex max-w-[16rem] items-center gap-2.5 rounded-2xl py-1 pr-3.5 pl-1 outline-none\",\n          \"bg-zinc-100/90 ring-1 ring-black/[0.06] transition-colors\",\n          \"hover:bg-zinc-100 dark:bg-zinc-800/90 dark:ring-white/10 dark:hover:bg-zinc-800\",\n          \"focus-visible:ring-2 focus-visible:ring-zinc-950 focus-visible:ring-offset-2 focus-visible:ring-offset-background dark:focus-visible:ring-white\",\n        )}\n      >\n        <LiquidMetalAvatar\n          size=\"sm\"\n          className=\"size-9 shrink-0 [&_[data-slot=avatar]]:inset-[2.5px]\"\n          speed={0.28}\n          softness={0.62}\n        >\n          <AvatarImage src={avatarSrc} alt={avatarAlt ?? name} />\n          <AvatarFallback>{initials}</AvatarFallback>\n        </LiquidMetalAvatar>\n        <span className=\"min-w-0 flex-1 text-left\">\n          <span className=\"block truncate text-[0.8125rem] font-semibold tracking-tight text-zinc-950 dark:text-zinc-50\">\n            {name}\n          </span>\n          <span className=\"block truncate text-[0.68rem] text-zinc-500 dark:text-zinc-400\">\n            {handle}\n          </span>\n        </span>\n      </motion.button>\n\n      <AnimatePresence>\n        {open ? (\n          <motion.div\n            id={menuId}\n            role=\"menu\"\n            aria-label=\"Profile menu\"\n            initial={\n              reduceMotion\n                ? false\n                : { opacity: 0, scale: 0.96, y: -6 }\n            }\n            animate={{ opacity: 1, scale: 1, y: 0 }}\n            exit={\n              reduceMotion\n                ? undefined\n                : { opacity: 0, scale: 0.96, y: -6 }\n            }\n            transition={reduceMotion ? { duration: 0 } : SPRING_PANEL}\n            className={cn(\n              \"absolute top-[calc(100%+0.5rem)] right-0 z-50 w-[17.5rem] origin-top-right overflow-hidden\",\n              \"rounded-2xl border border-black/[0.06] bg-white/95 shadow-[0_12px_40px_rgba(0,0,0,0.14)] backdrop-blur-xl\",\n              \"dark:border-white/10 dark:bg-zinc-900/95 dark:shadow-[0_12px_40px_rgba(0,0,0,0.45)]\",\n              preview && \"pointer-events-none\",\n            )}\n          >\n            {/* Header */}\n            <div className=\"flex items-center gap-3 px-3.5 py-3.5\">\n              <LiquidMetalAvatar\n                size=\"md\"\n                className=\"size-11 shrink-0 [&_[data-slot=avatar]]:inset-[3.5px]\"\n                speed={0.28}\n                softness={0.62}\n              >\n                <AvatarImage src={avatarSrc} alt=\"\" />\n                <AvatarFallback>{initials}</AvatarFallback>\n              </LiquidMetalAvatar>\n              <div className=\"min-w-0 flex-1\">\n                <p className=\"truncate text-[0.9rem] font-semibold tracking-tight text-zinc-950 dark:text-zinc-50\">\n                  {name}\n                </p>\n                <p className=\"truncate text-[0.72rem] text-zinc-500 dark:text-zinc-400\">\n                  {handle}\n                </p>\n                <p className=\"truncate text-[0.68rem] text-zinc-400 dark:text-zinc-500\">\n                  {email}\n                </p>\n              </div>\n            </div>\n\n            <div className=\"mx-3 h-px bg-zinc-200/90 dark:bg-zinc-700/80\" />\n\n            {/* Account */}\n            <Section label=\"Account\">\n              {accountItems.map((item) => (\n                <MenuRow\n                  key={item.id}\n                  item={item}\n                  onSelect={() => runItem(item)}\n                />\n              ))}\n            </Section>\n\n            <div className=\"mx-3 h-px bg-zinc-200/90 dark:bg-zinc-700/80\" />\n\n            {/* Workspace */}\n            <Section label=\"Workspace\">\n              {workspaceItems.map((item) => (\n                <MenuRow\n                  key={item.id}\n                  item={item}\n                  onSelect={() => runItem(item)}\n                />\n              ))}\n            </Section>\n\n            <div className=\"mx-3 h-px bg-zinc-200/90 dark:bg-zinc-700/80\" />\n\n            <div className=\"p-1.5 pb-2\">\n              <button\n                type=\"button\"\n                role=\"menuitem\"\n                onClick={() => {\n                  onSignOut?.();\n                  if (!preview) setOpen(false);\n                }}\n                className={cn(\n                  \"flex w-full items-center gap-2.5 rounded-xl px-2.5 py-2 text-left text-[0.8125rem] font-medium\",\n                  \"bg-red-50 text-red-600 outline-none transition-colors\",\n                  \"hover:bg-red-100 hover:text-red-700 focus-visible:bg-red-100 focus-visible:text-red-700\",\n                  \"dark:bg-red-500/15 dark:text-red-400\",\n                  \"dark:hover:bg-red-500/25 dark:hover:text-red-300\",\n                  \"dark:focus-visible:bg-red-500/25 dark:focus-visible:text-red-300\",\n                )}\n              >\n                <LogOut\n                  className=\"size-4 shrink-0 text-red-600 dark:text-red-400\"\n                  strokeWidth={1.75}\n                />\n                Sign out\n              </button>\n            </div>\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  );\n}\n\nfunction Section({\n  label,\n  children,\n}: {\n  label: string;\n  children: ReactNode;\n}) {\n  return (\n    <div className=\"px-1.5 py-1.5\">\n      <p className=\"px-2.5 pb-1 pt-0.5 text-[0.65rem] font-medium tracking-wide text-zinc-400 uppercase dark:text-zinc-500\">\n        {label}\n      </p>\n      <div className=\"flex flex-col gap-0.5\">{children}</div>\n    </div>\n  );\n}\n\nfunction MenuRow({\n  item,\n  onSelect,\n}: {\n  item: ProfileDropdownItem;\n  onSelect: () => void;\n}) {\n  return (\n    <button\n      type=\"button\"\n      role=\"menuitem\"\n      onClick={onSelect}\n      className={cn(\n        \"flex w-full items-center gap-2.5 rounded-xl px-2.5 py-2 text-left text-[0.8125rem] font-medium\",\n        \"text-zinc-800 outline-none transition-colors\",\n        \"hover:bg-zinc-100 focus-visible:bg-zinc-100\",\n        \"dark:text-zinc-100 dark:hover:bg-zinc-800 dark:focus-visible:bg-zinc-800\",\n      )}\n    >\n      <span className=\"flex size-4 shrink-0 items-center justify-center text-zinc-500 dark:text-zinc-400\">\n        {item.icon}\n      </span>\n      {item.label}\n    </button>\n  );\n}\n"}],"meta":{"studioName":"GFA Development & Design Studio","siteUrl":"https://ui.gkhn.dev"}}