{
  "name": "hover-preview",
  "files": [
    {
      "path": "ui/hover-preview.tsx",
      "content": "\"use client\";\n\nimport NextImage from \"next/image\";\nimport type React from \"react\";\nimport {\n  createContext,\n  forwardRef,\n  useCallback,\n  useContext,\n  useEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { cn } from \"@/lib/cn\";\n\n// ==========================================\n// TYPES & INTERFACES\n// ==========================================\n\nexport interface PreviewData {\n  /** Image URL for the preview card */\n  image: string;\n  /** Title displayed in the preview card */\n  title: string;\n  /** Subtitle or description displayed below the title */\n  subtitle?: string;\n}\n\nexport interface HoverPreviewLinkProps {\n  /** Unique key to identify which preview data to show */\n  previewKey: string;\n  /** Content to render as the hoverable link */\n  children: React.ReactNode;\n  /** Additional CSS classes for the link */\n  className?: string;\n}\n\nexport interface HoverPreviewCardProps {\n  /** Width of the preview card in pixels */\n  width?: number;\n  /** Border radius of the card */\n  borderRadius?: number;\n  /** Additional CSS classes for the card */\n  className?: string;\n}\n\nexport interface HoverPreviewProviderProps {\n  /** Preview data object with keys matching the previewKey in HoverPreviewLink */\n  data: Record<string, PreviewData>;\n  /** Children components (should include HoverPreviewLink components) */\n  children: React.ReactNode;\n  /** Card configuration options */\n  cardProps?: HoverPreviewCardProps;\n  /** Offset distance from cursor in pixels */\n  cursorOffset?: number;\n  /** Whether to preload all images on mount */\n  preloadImages?: boolean;\n  /** Additional CSS classes for the container */\n  className?: string;\n}\n\n// ==========================================\n// CONTEXT\n// ==========================================\n\ninterface HoverPreviewContextValue {\n  data: Record<string, PreviewData>;\n  activePreview: PreviewData | null;\n  position: { x: number; y: number };\n  isVisible: boolean;\n  cardProps: HoverPreviewCardProps;\n  handleHoverStart: (key: string, e: React.MouseEvent) => void;\n  handleHoverMove: (e: React.MouseEvent) => void;\n  handleHoverEnd: () => void;\n}\n\nconst HoverPreviewContext = createContext<HoverPreviewContextValue | null>(\n  null,\n);\n\nfunction useHoverPreview() {\n  const context = useContext(HoverPreviewContext);\n  if (!context) {\n    throw new Error(\n      \"HoverPreviewLink must be used within a HoverPreviewProvider\",\n    );\n  }\n  return context;\n}\n\n// ==========================================\n// COMPONENTS\n// ==========================================\n\nexport function HoverPreviewProvider({\n  data,\n  children,\n  cardProps = {},\n  cursorOffset = 20,\n  preloadImages = true,\n  className,\n}: HoverPreviewProviderProps) {\n  const [activePreview, setActivePreview] = useState<PreviewData | null>(null);\n  const [position, setPosition] = useState({ x: 0, y: 0 });\n  const [isVisible, setIsVisible] = useState(false);\n  const cardRef = useRef<HTMLDivElement>(null);\n\n  const cardWidth = cardProps.width ?? 300;\n  const cardHeight = 250;\n\n  // Preload all images on mount\n  useEffect(() => {\n    if (!preloadImages) return;\n    Object.values(data).forEach((item) => {\n      const img = new Image();\n      img.crossOrigin = \"anonymous\";\n      img.src = item.image;\n    });\n  }, [data, preloadImages]);\n\n  const updatePosition = useCallback(\n    (e: React.MouseEvent | MouseEvent) => {\n      let x = e.clientX - cardWidth / 2;\n      let y = e.clientY - cardHeight - cursorOffset;\n\n      // Boundary checks\n      if (x + cardWidth > window.innerWidth - 20) {\n        x = window.innerWidth - cardWidth - 20;\n      }\n      if (x < 20) {\n        x = 20;\n      }\n      if (y < 20) {\n        y = e.clientY + cursorOffset;\n      }\n\n      setPosition({ x, y });\n    },\n    [cardWidth, cursorOffset],\n  );\n\n  const handleHoverStart = useCallback(\n    (key: string, e: React.MouseEvent) => {\n      const previewData = data[key];\n      if (previewData) {\n        setActivePreview(previewData);\n        setIsVisible(true);\n        updatePosition(e);\n      }\n    },\n    [data, updatePosition],\n  );\n\n  const handleHoverMove = useCallback(\n    (e: React.MouseEvent) => {\n      if (isVisible) {\n        updatePosition(e);\n      }\n    },\n    [isVisible, updatePosition],\n  );\n\n  const handleHoverEnd = useCallback(() => {\n    setIsVisible(false);\n  }, []);\n\n  const contextValue: HoverPreviewContextValue = {\n    data,\n    activePreview,\n    position,\n    isVisible,\n    cardProps: { width: cardWidth, ...cardProps },\n    handleHoverStart,\n    handleHoverMove,\n    handleHoverEnd,\n  };\n\n  return (\n    <HoverPreviewContext.Provider value={contextValue}>\n      <div className={cn(\"relative\", className)}>\n        {children}\n        <HoverPreviewCard ref={cardRef} />\n      </div>\n    </HoverPreviewContext.Provider>\n  );\n}\n\nexport function HoverPreviewLink({\n  previewKey,\n  children,\n  className,\n}: HoverPreviewLinkProps) {\n  const { handleHoverStart, handleHoverMove, handleHoverEnd } =\n    useHoverPreview();\n\n  return (\n    <span\n      role=\"button\"\n      tabIndex={0}\n      className={cn(\n        \"relative inline-block cursor-pointer font-semibold text-foreground transition-colors\",\n        \"after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-0 after:bg-gradient-to-r after:from-primary after:to-primary/60 after:transition-all after:duration-300\",\n        \"hover:after:w-full\",\n        className,\n      )}\n      onMouseEnter={(e) => handleHoverStart(previewKey, e)}\n      onMouseMove={handleHoverMove}\n      onMouseLeave={handleHoverEnd}\n      onFocus={(e) =>\n        handleHoverStart(previewKey, e as unknown as React.MouseEvent)\n      }\n    >\n      {children}\n    </span>\n  );\n}\n\nconst HoverPreviewCard = forwardRef<HTMLDivElement>((_, ref) => {\n  const { activePreview, position, isVisible, cardProps } = useHoverPreview();\n\n  if (!activePreview) return null;\n\n  return (\n    <div\n      ref={ref}\n      className={cn(\n        \"pointer-events-none fixed z-50 transition-all duration-200\",\n        isVisible\n          ? \"scale-100 opacity-100\"\n          : \"translate-y-2 scale-95 opacity-0\",\n      )}\n      style={{\n        left: `${position.x}px`,\n        top: `${position.y}px`,\n        width: cardProps.width,\n      }}\n    >\n      <div\n        className={cn(\n          \"overflow-hidden border border-border/50 bg-card/95 p-2 shadow-2xl backdrop-blur-md\",\n          cardProps.className,\n        )}\n        style={{ borderRadius: cardProps.borderRadius ?? 16 }}\n      >\n        <NextImage\n          src={activePreview.image}\n          alt={activePreview.title || \"\"}\n          width={300}\n          height={169}\n          unoptimized\n          className=\"aspect-video w-full rounded-lg object-cover\"\n        />\n        <div className=\"px-2 pt-3 pb-1\">\n          <div className=\"font-semibold text-foreground text-sm\">\n            {activePreview.title}\n          </div>\n          {activePreview.subtitle && (\n            <div className=\"mt-1 text-muted-foreground text-xs\">\n              {activePreview.subtitle}\n            </div>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n});\n\nHoverPreviewCard.displayName = \"HoverPreviewCard\";\n\n// ==========================================\n// EXPORTS\n// ==========================================\n\nexport { HoverPreviewContext, useHoverPreview };\n",
      "type": "registry:ui",
      "target": ""
    }
  ],
  "type": "registry:ui"
}