{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "hover-card-placement",
  "title": "Card-placement hover",
  "description": "Styled hover: the Card-placement set. 5 decorative hover variations (TopHoverCard, BottomHoverCard, LeftHoverCard, RightHoverCard, FollowHoverCard) on the ai2 token system, powered by framer-motion, reduced-motion aware and sized sm to xl. Part of the free styled layer.",
  "dependencies": [
    "motion@^12.42.2",
    "lucide-react@^1.23.0"
  ],
  "registryDependencies": [
    "@ai2/tokens"
  ],
  "files": [
    {
      "path": "registry/ai2/styled/hover-card-placement.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ArrowDown, ArrowLeft, ArrowRight, ArrowUp, Move } from \"lucide-react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/* Placement hover card family: 5 cards that open in different DIRECTIONS relative\n   to the trigger (top, bottom, left, right, side-offset). Self-contained (no radix,\n   no portal) - hover/focus on the trigger opens the JS state and AnimatePresence\n   brings the card in with a slight slide from that direction. The positioning is\n   per-direction through className: a relative wrapper + an absolute position wrapper\n   (the centering translate lives here) + the inner motion card, which carries only\n   an opacity + small x/y offset animation (so no transform collision). Color comes\n   ONLY from tokens; alpha via color-mix(in oklab, ...). Under useReducedMotion all\n   transforms drop away and only an opacity fade remains. size = card width. */\n\nexport type StyledSize = \"sm\" | \"md\" | \"lg\" | \"xl\"\n\ntype Placement = \"top\" | \"bottom\" | \"left\" | \"right\" | \"follow\"\n\nconst cardWidth: Record<StyledSize, string> = {\n  sm: \"w-56\",\n  md: \"w-64\",\n  lg: \"w-72\",\n  xl: \"w-80\",\n}\n\n/* Position wrapper: direction relative to the trigger plus a centring translate. The inner card carries motion x/y, which is why the centring translate lives here (on a separate element). */\nconst posClass: Record<Placement, string> = {\n  top: \"bottom-full left-1/2 mb-2 -translate-x-1/2\",\n  bottom: \"top-full left-1/2 mt-2 -translate-x-1/2\",\n  left: \"right-full top-1/2 mr-2 -translate-y-1/2\",\n  right: \"left-full top-1/2 ml-2 -translate-y-1/2\",\n  follow: \"left-full top-0 ml-2\",\n}\n\n/* Entry offset per direction (px). It enters sliding slightly from that side. */\nconst offset: Record<Placement, { x: number; y: number }> = {\n  top: { x: 0, y: -8 },\n  bottom: { x: 0, y: 8 },\n  left: { x: -8, y: 0 },\n  right: { x: 8, y: 0 },\n  follow: { x: 6, y: 6 },\n}\n\nconst cardBase =\n  \"pointer-events-auto block rounded-xl border border-border bg-popover p-4 text-left text-popover-foreground shadow-lg\"\n\nconst triggerBase =\n  \"inline-flex items-center gap-2 rounded-md font-medium text-primary underline-offset-4 outline-none hover:underline focus-visible:ring-[3px] focus-visible:ring-ring/50 [&_svg]:size-4 [&_svg]:shrink-0 [&_i]:text-base [&_i]:leading-none\"\n\ninterface Props {\n  size?: StyledSize\n  trigger?: React.ReactNode\n  children?: React.ReactNode\n  className?: string\n}\n\ninterface ShellProps extends Props {\n  placement: Placement\n  icon: React.ReactNode\n  title: React.ReactNode\n  fallback: React.ReactNode\n}\n\n/* Shared shell: trigger plus an AnimatePresence card positioned in that direction. It opens on hover and focus (JS state). data-slot=\"styled-hover-card\" is preserved. */\nfunction HoverCardShell({\n  placement,\n  size = \"md\",\n  trigger,\n  icon,\n  title,\n  fallback,\n  children,\n  className,\n}: ShellProps) {\n  const [open, setOpen] = React.useState(false)\n  const reduce = useReducedMotion()\n  const off = offset[placement]\n\n  const initial = reduce ? { opacity: 0 } : { opacity: 0, x: off.x, y: off.y }\n  const animate = reduce ? { opacity: 1 } : { opacity: 1, x: 0, y: 0 }\n\n  return (\n    <span\n      data-slot=\"styled-hover-card\"\n      className=\"group relative inline-flex\"\n      onPointerEnter={() => setOpen(true)}\n      onPointerLeave={() => setOpen(false)}\n      onFocus={() => setOpen(true)}\n      onBlur={() => setOpen(false)}\n    >\n      {trigger ?? (\n        <button type=\"button\" className={triggerBase}>\n          {icon}\n          {fallback}\n        </button>\n      )}\n      <span className={cn(\"pointer-events-none absolute z-20\", posClass[placement])}>\n        <AnimatePresence>\n          {open ? (\n            <motion.span\n              role=\"dialog\"\n              className={cn(cardBase, cardWidth[size], className)}\n              initial={initial}\n              animate={animate}\n              exit={initial}\n              transition={reduce ? { duration: 0.12 } : { duration: 0.18, ease: \"easeOut\" }}\n            >\n              <span className=\"flex items-center gap-2 text-sm font-semibold [&_svg]:size-4 [&_svg]:shrink-0 [&_i]:text-base [&_i]:leading-none\">\n                {icon}\n                {title}\n              </span>\n              <span className=\"mt-2 block text-sm text-muted-foreground\">{children}</span>\n            </motion.span>\n          ) : null}\n        </AnimatePresence>\n      </span>\n    </span>\n  )\n}\n\n/* Top: the card opens ABOVE the trigger, entering with a slight slide downwards. */\nexport function TopHoverCard({ size, trigger, children, className }: Props) {\n  return (\n    <HoverCardShell\n      placement=\"top\"\n      size={size}\n      trigger={trigger}\n      className={className}\n      icon={<ArrowUp />}\n      title=\"Top placement\"\n      fallback=\"Above\"\n    >\n      {children ?? \"Opens above the trigger and eases down into its resting spot.\"}\n    </HoverCardShell>\n  )\n}\n\n/* Bottom: the card opens BELOW the trigger, entering with a slight slide upwards. */\nexport function BottomHoverCard({ size, trigger, children, className }: Props) {\n  return (\n    <HoverCardShell\n      placement=\"bottom\"\n      size={size}\n      trigger={trigger}\n      className={className}\n      icon={<ArrowDown />}\n      title=\"Bottom placement\"\n      fallback=\"Below\"\n    >\n      {children ?? \"Opens below the trigger and eases up into its resting spot.\"}\n    </HoverCardShell>\n  )\n}\n\n/* Left: the card opens to the LEFT of the trigger, entering with a slight slide from the left. */\nexport function LeftHoverCard({ size, trigger, children, className }: Props) {\n  return (\n    <HoverCardShell\n      placement=\"left\"\n      size={size}\n      trigger={trigger}\n      className={className}\n      icon={<ArrowLeft />}\n      title=\"Left placement\"\n      fallback=\"Left\"\n    >\n      {children ?? \"Opens to the left of the trigger and slides in from that side.\"}\n    </HoverCardShell>\n  )\n}\n\n/* Right: the card opens to the RIGHT of the trigger, entering with a slight slide from the right. */\nexport function RightHoverCard({ size, trigger, children, className }: Props) {\n  return (\n    <HoverCardShell\n      placement=\"right\"\n      size={size}\n      trigger={trigger}\n      className={className}\n      icon={<ArrowRight />}\n      title=\"Right placement\"\n      fallback=\"Right\"\n    >\n      {children ?? \"Opens to the right of the trigger and slides in from that side.\"}\n    </HoverCardShell>\n  )\n}\n\n/* Follow: the card opens BESIDE the trigger with a slight diagonal offset. */\nexport function FollowHoverCard({ size, trigger, children, className }: Props) {\n  return (\n    <HoverCardShell\n      placement=\"follow\"\n      size={size}\n      trigger={trigger}\n      className={className}\n      icon={<Move />}\n      title=\"Follow placement\"\n      fallback=\"Beside\"\n    >\n      {children ?? \"Sits beside the trigger with a small offset and drifts into place.\"}\n    </HoverCardShell>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ui/hover-card-placement.tsx"
    }
  ],
  "categories": [
    "styled",
    "hover"
  ],
  "type": "registry:component"
}