{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "orbit-card-stack",
  "type": "registry:ui",
  "title": "Orbit Card Stack",
  "dependencies": [
    "framer-motion",
    "lucide-react"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Premium hover card deck that fans outward and lifts the active card without changing its color or angle.",
  "files": [
    {
      "path": "components/ui/orbit-card-stack.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { ArrowUpRight } from \"lucide-react\";\nimport { useReducedMotion } from \"framer-motion\";\nimport {\n  type CSSProperties,\n  type FocusEvent,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\n\nexport interface OrbitStackItem {\n  name: string;\n  role: string;\n  description: string;\n  accent?: string;\n  initials?: string;\n  stat?: string;\n  image?: string;\n}\n\ninterface OrbitCardStackProps {\n  items?: OrbitStackItem[];\n  className?: string;\n  cardClassName?: string;\n  defaultActiveIndex?: number;\n  spread?: number;\n  lift?: number;\n  onActiveChange?: (item: OrbitStackItem, index: number) => void;\n}\n\nconst defaultItems: OrbitStackItem[] = [\n  {\n    name: \"Mira Vale\",\n    role: \"Creative Lead\",\n    description:\n      \"Shapes visual systems with enough restraint to feel expensive and enough edge to be remembered.\",\n    accent: \"#f8d66d\",\n    initials: \"MV\",\n    stat: \"Identity\",\n    image: \"/images/orbit-card-stack/mira-vale.png\",\n  },\n  {\n    name: \"Noor Kade\",\n    role: \"Product Strategy\",\n    description:\n      \"Turns loose ideas into sharp product moves, crisp priorities, and launchable experiences.\",\n    accent: \"#78dcca\",\n    initials: \"NK\",\n    stat: \"Roadmap\",\n    image: \"/images/orbit-card-stack/noor-kade.png\",\n  },\n  {\n    name: \"Ari Chen\",\n    role: \"Founder\",\n    description:\n      \"Sets the taste bar, protects the details, and keeps the whole team pointed at the same high signal.\",\n    accent: \"#f3f1ea\",\n    initials: \"AC\",\n    stat: \"Vision\",\n    image: \"/images/orbit-card-stack/ari-chen.png\",\n  },\n  {\n    name: \"Sana Holt\",\n    role: \"Frontend Engineer\",\n    description:\n      \"Builds the motion, polish, and interface texture that make the product feel calm under pressure.\",\n    accent: \"#b9a7ff\",\n    initials: \"SH\",\n    stat: \"Motion\",\n    image: \"/images/orbit-card-stack/sana-holt.png\",\n  },\n  {\n    name: \"Ezra Moon\",\n    role: \"Operations\",\n    description:\n      \"Keeps the machine quiet, the handoffs clean, and the team moving without pointless friction.\",\n    accent: \"#ff9d77\",\n    initials: \"EM\",\n    stat: \"Systems\",\n    image: \"/images/orbit-card-stack/ezra-moon.png\",\n  },\n];\n\nfunction inRange(index: number, length: number) {\n  return Math.min(Math.max(0, index), Math.max(0, length - 1));\n}\n\nfunction initialsFor(item: OrbitStackItem) {\n  return (\n    item.initials ??\n    item.name\n      .split(/\\s+/)\n      .map((part) => part.at(0))\n      .join(\"\")\n      .slice(0, 2)\n      .toUpperCase()\n  );\n}\n\nfunction Portrait({ item }: { item: OrbitStackItem }) {\n  const initials = initialsFor(item);\n  const shared =\n    \"relative flex aspect-[1.36] w-full overflow-hidden rounded-[1.45rem] border border-black/[0.08] bg-black/[0.045]\";\n\n  if (item.image) {\n    return (\n      <div className={shared}>\n        <img\n          src={item.image}\n          alt={item.name}\n          className=\"h-full w-full object-cover\"\n        />\n        <span className=\"absolute bottom-4 right-4 rounded-full bg-zinc-950 px-3 py-1 text-xs font-semibold tracking-[0.18em] text-white\">\n          {initials}\n        </span>\n      </div>\n    );\n  }\n\n  return (\n    <div\n      className={shared}\n      style={{ \"--portrait-accent\": item.accent ?? \"#f3f1ea\" } as CSSProperties}\n    >\n      <div className=\"absolute inset-0 bg-[radial-gradient(circle_at_22%_20%,var(--portrait-accent),transparent_24%),radial-gradient(circle_at_85%_72%,rgba(255,255,255,0.5),transparent_28%)] opacity-45\" />\n      <div className=\"absolute inset-x-8 bottom-0 h-[72%] rounded-t-[999px] border-2 border-zinc-950 bg-[#f7f5ef]\" />\n      <div className=\"absolute left-1/2 top-[22%] size-24 -translate-x-1/2 rounded-[45%_55%_48%_52%] border-2 border-zinc-950 bg-[#f5f2eb]\">\n        <span className=\"absolute left-[27%] top-[34%] size-2 rounded-full bg-zinc-950\" />\n        <span className=\"absolute right-[27%] top-[34%] size-2 rounded-full bg-zinc-950\" />\n        <span className=\"absolute left-1/2 top-[52%] h-6 w-4 -translate-x-1/2 rounded-b-full border-b-2 border-zinc-950\" />\n        <span\n          className=\"absolute -top-5 left-1/2 h-9 w-24 -translate-x-1/2 rounded-t-full border-2 border-b-0 border-zinc-950\"\n          style={{ backgroundColor: item.accent ?? \"#f3f1ea\" }}\n        />\n      </div>\n      <span className=\"absolute bottom-4 right-4 rounded-full bg-zinc-950 px-3 py-1 text-xs font-semibold tracking-[0.18em] text-white\">\n        {initials}\n      </span>\n    </div>\n  );\n}\n\nexport function OrbitCardStack({\n  items = defaultItems,\n  className,\n  cardClassName,\n  defaultActiveIndex = 2,\n  spread = 168,\n  lift = 34,\n  onActiveChange,\n}: OrbitCardStackProps) {\n  const reduceMotion = useReducedMotion() ?? false;\n  const cards = items.length ? items : defaultItems;\n  const restingIndex = inRange(defaultActiveIndex, cards.length);\n  const [activeIndex, setActiveIndex] = useState(restingIndex);\n  const [open, setOpen] = useState(false);\n  const stageRef = useRef<HTMLDivElement>(null);\n  const midpoint = (cards.length - 1) / 2;\n\n  const layouts = useMemo(\n    () =>\n      cards.map((_, index) => {\n        const orbit = index - midpoint;\n        const stack = index - restingIndex;\n        return {\n          open: {\n            x: orbit * spread,\n            y: Math.abs(orbit) * 30 + Math.max(0, Math.abs(orbit) - 1) * 10,\n            rotation: orbit * 8.5,\n          },\n          closed: {\n            x: stack * 10,\n            y: Math.abs(stack) * 5,\n            rotation: stack * 2.8,\n          },\n        };\n      }),\n    [cards, midpoint, restingIndex, spread],\n  );\n\n  const activate = (index: number) => {\n    const next = inRange(index, cards.length);\n    setOpen(true);\n    setActiveIndex(next);\n    onActiveChange?.(cards[next]!, next);\n  };\n  const close = () => {\n    setOpen(false);\n    setActiveIndex(restingIndex);\n  };\n  const leaveFocus = (event: FocusEvent<HTMLDivElement>) => {\n    if (!event.currentTarget.contains(event.relatedTarget)) close();\n  };\n\n  return (\n    <div\n      className={cn(\n        \"relative flex min-h-full w-full items-center justify-center overflow-hidden p-8\",\n        className,\n      )}\n    >\n      <div\n        ref={stageRef}\n        className=\"relative h-[470px] w-full max-w-[980px]\"\n        onMouseLeave={close}\n        onBlur={leaveFocus}\n        role=\"list\"\n        aria-label=\"Profile card stack\"\n      >\n        {cards.map((item, index) => {\n          const position = open ? layouts[index]!.open : layouts[index]!.closed;\n          const active = index === activeIndex;\n          const style: CSSProperties = {\n            zIndex: active ? 80 : 50 - Math.abs(index - activeIndex),\n            transform: `translate(calc(-50% + ${position.x}px), calc(-50% + ${\n              position.y - (open && active ? lift : 0)\n            }px)) rotate(${position.rotation}deg) scale(${open ? 0.985 : 0.97})`,\n            transitionDuration: reduceMotion ? \"0ms\" : \"420ms\",\n          };\n\n          return (\n            <article\n              key={`${item.name}-${index}`}\n              role=\"listitem\"\n              tabIndex={0}\n              aria-current={active ? \"true\" : undefined}\n              className={cn(\n                \"absolute left-1/2 top-1/2 w-[min(78vw,21rem)] origin-bottom cursor-pointer rounded-[1.9rem] border border-black/10 bg-[#e9e6df] p-4 text-[#141414] outline-none\",\n                \"transition-[transform] ease-[cubic-bezier(.2,.8,.2,1)] focus-visible:ring-2 focus-visible:ring-zinc-950/30 focus-visible:ring-offset-2 focus-visible:ring-offset-white\",\n                cardClassName,\n              )}\n              style={style}\n              onMouseEnter={() => activate(index)}\n              onFocus={() => activate(index)}\n              onClick={() => activate(index)}\n              onKeyDown={(event) => {\n                if (event.key === \"ArrowRight\" || event.key === \"ArrowDown\") {\n                  event.preventDefault();\n                  const next = (index + 1) % cards.length;\n                  activate(next);\n                  stageRef.current\n                    ?.querySelectorAll<HTMLElement>(\"[role=listitem]\")\n                    [next]?.focus();\n                }\n                if (event.key === \"ArrowLeft\" || event.key === \"ArrowUp\") {\n                  event.preventDefault();\n                  const next = (index - 1 + cards.length) % cards.length;\n                  activate(next);\n                  stageRef.current\n                    ?.querySelectorAll<HTMLElement>(\"[role=listitem]\")\n                    [next]?.focus();\n                }\n                if (event.key === \"Escape\") {\n                  event.currentTarget.blur();\n                  close();\n                }\n              }}\n            >\n              <div className=\"relative\">\n                <Portrait item={item} />\n                <span className=\"absolute right-3 top-3 grid size-11 place-items-center rounded-full bg-zinc-950 text-white shadow-lg shadow-black/20\">\n                  <ArrowUpRight className=\"size-4\" aria-hidden />\n                </span>\n              </div>\n              <div className=\"px-2 pb-2 pt-6\">\n                <p className=\"text-[0.72rem] font-semibold uppercase tracking-[0.18em] text-zinc-500\">\n                  {item.role}\n                </p>\n                <h3 className=\"mt-2 text-[2rem] font-semibold leading-none tracking-[-0.04em] text-zinc-950\">\n                  {item.name}\n                </h3>\n                <p className=\"mt-4 max-w-[17rem] text-[0.98rem] font-medium leading-[1.42] tracking-[-0.01em] text-zinc-700\">\n                  {item.description}\n                </p>\n                <div className=\"mt-5 border-t border-black/10 pt-4 text-[0.68rem] font-bold uppercase tracking-[0.2em] text-zinc-500\">\n                  {item.stat ?? \"Profile\"}\n                </div>\n              </div>\n            </article>\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ]
}