{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "info-card",
  "type": "registry:block",
  "description": "Information cards can serve as callout cards, banners, toast notifications, or announcement boxes to deliver news, updates, and alerts to your users.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/kl-ui/info-card/info-card.tsx",
      "content": "\"use client\";\n\nimport {\n  useState,\n  useRef,\n  useEffect,\n  createContext,\n  useContext,\n  useMemo,\n  useCallback,\n} from \"react\";\nimport { motion, AnimatePresence } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\nimport React from \"react\";\n\ninterface InfoCardTitleProps extends React.HTMLAttributes<HTMLDivElement> {\n  children: React.ReactNode;\n}\n\ninterface InfoCardDescriptionProps\n  extends React.HTMLAttributes<HTMLDivElement> {\n  children: React.ReactNode;\n}\n\nconst InfoCardTitle = React.memo(\n  ({ children, className, ...props }: InfoCardTitleProps) => {\n    return (\n      <div className={cn(\"font-medium mb-1\", className)} {...props}>\n        {children}\n      </div>\n    );\n  }\n);\nInfoCardTitle.displayName = \"InfoCardTitle\";\n\nconst InfoCardDescription = React.memo(\n  ({ children, className, ...props }: InfoCardDescriptionProps) => {\n    return (\n      <div\n        className={cn(\"text-muted-foreground leading-4\", className)}\n        {...props}\n      >\n        {children}\n      </div>\n    );\n  }\n);\nInfoCardDescription.displayName = \"InfoCardDescription\";\n\ninterface CommonCardProps extends React.HTMLAttributes<HTMLDivElement> {\n  children: React.ReactNode;\n}\n\ninterface InfoCardProps extends React.HTMLAttributes<HTMLDivElement> {\n  children: React.ReactNode;\n  storageKey?: string;\n  dismissType?: \"once\" | \"forever\";\n}\n\ntype InfoCardContentProps = CommonCardProps;\ntype InfoCardFooterProps = CommonCardProps;\ntype InfoCardDismissProps = React.HTMLAttributes<HTMLDivElement> & {\n  children: React.ReactNode;\n  onDismiss?: () => void;\n};\ntype InfoCardActionProps = CommonCardProps;\n\nconst InfoCardContent = React.memo(\n  ({ children, className, ...props }: InfoCardContentProps) => {\n    return (\n      <div className={cn(\"flex flex-col gap-1 text-xs\", className)} {...props}>\n        {children}\n      </div>\n    );\n  }\n);\nInfoCardContent.displayName = \"InfoCardContent\";\n\ninterface MediaItem {\n  type?: \"image\" | \"video\";\n  src: string;\n  alt?: string;\n  className?: string;\n  [key: string]: any;\n}\n\ninterface InfoCardMediaProps extends React.HTMLAttributes<HTMLDivElement> {\n  media: MediaItem[];\n  loading?: \"eager\" | \"lazy\";\n  shrinkHeight?: number;\n  expandHeight?: number;\n}\n\nconst InfoCardImageContext = createContext<{\n  handleMediaLoad: (mediaSrc: string) => void;\n  setAllImagesLoaded: (loaded: boolean) => void;\n}>({\n  handleMediaLoad: () => {},\n  setAllImagesLoaded: () => {},\n});\n\nconst InfoCardContext = createContext<{\n  isHovered: boolean;\n  onDismiss: () => void;\n}>({\n  isHovered: false,\n  onDismiss: () => {},\n});\n\nfunction InfoCard({\n  children,\n  className,\n  storageKey,\n  dismissType = \"once\",\n}: InfoCardProps) {\n  if (dismissType === \"forever\" && !storageKey) {\n    throw new Error(\n      'A storageKey must be provided when using dismissType=\"forever\"'\n    );\n  }\n\n  const [isHovered, setIsHovered] = useState(false);\n  const [allImagesLoaded, setAllImagesLoaded] = useState(true);\n  const [isDismissed, setIsDismissed] = useState(() => {\n    if (typeof window === \"undefined\" || dismissType === \"once\") return false;\n    return dismissType === \"forever\"\n      ? localStorage.getItem(storageKey!) === \"dismissed\"\n      : false;\n  });\n\n  const handleDismiss = useCallback(() => {\n    setIsDismissed(true);\n    if (dismissType === \"forever\") {\n      localStorage.setItem(storageKey!, \"dismissed\");\n    }\n  }, [storageKey, dismissType]);\n\n  const imageContextValue = useMemo(\n    () => ({\n      handleMediaLoad: () => {},\n      setAllImagesLoaded,\n    }),\n    [setAllImagesLoaded]\n  );\n\n  const cardContextValue = useMemo(\n    () => ({\n      isHovered,\n      onDismiss: handleDismiss,\n    }),\n    [isHovered, handleDismiss]\n  );\n\n  return (\n    <InfoCardContext.Provider value={cardContextValue}>\n      <InfoCardImageContext.Provider value={imageContextValue}>\n        <AnimatePresence>\n          {!isDismissed && (\n            <motion.div\n              initial={{ opacity: 0, y: 10 }}\n              animate={{\n                opacity: allImagesLoaded ? 1 : 0,\n                y: allImagesLoaded ? 0 : 10,\n              }}\n              exit={{\n                opacity: 0,\n                y: 10,\n                transition: { duration: 0.2 },\n              }}\n              transition={{ duration: 0.3, delay: 0 }}\n              className={cn(\n                \"group rounded-lg border p-3\",\n                \"bg-white\",\n                \"dark:bg-gradient-to-br dark:from-zinc-800 dark:to-zinc-900\",\n                \"dark:border-zinc-700\",\n                className\n              )}\n              onMouseEnter={() => setIsHovered(true)}\n              onMouseLeave={() => setIsHovered(false)}\n            >\n              {children}\n            </motion.div>\n          )}\n        </AnimatePresence>\n      </InfoCardImageContext.Provider>\n    </InfoCardContext.Provider>\n  );\n}\n\nconst InfoCardFooter = ({ children, className }: InfoCardFooterProps) => {\n  const { isHovered } = useContext(InfoCardContext);\n\n  return (\n    <motion.div\n      className={cn(\n        \"flex justify-between text-xs text-muted-foreground\",\n        className\n      )}\n      initial={{ opacity: 0, height: \"0px\" }}\n      animate={{\n        opacity: isHovered ? 1 : 0,\n        height: isHovered ? \"auto\" : \"0px\",\n      }}\n      transition={{\n        type: \"spring\",\n        stiffness: 300,\n        damping: 30,\n        duration: 0.3,\n      }}\n    >\n      {children}\n    </motion.div>\n  );\n};\n\nconst InfoCardDismiss = React.memo(\n  ({ children, className, onDismiss, ...props }: InfoCardDismissProps) => {\n    const { onDismiss: contextDismiss } = useContext(InfoCardContext);\n\n    const handleClick = (e: React.MouseEvent) => {\n      e.preventDefault();\n      onDismiss?.();\n      contextDismiss();\n    };\n\n    return (\n      <div\n        className={cn(\"cursor-pointer\", className)}\n        onClick={handleClick}\n        {...props}\n      >\n        {children}\n      </div>\n    );\n  }\n);\nInfoCardDismiss.displayName = \"InfoCardDismiss\";\n\nconst InfoCardAction = React.memo(\n  ({ children, className, ...props }: InfoCardActionProps) => {\n    return (\n      <div className={cn(\"\", className)} {...props}>\n        {children}\n      </div>\n    );\n  }\n);\nInfoCardAction.displayName = \"InfoCardAction\";\n\nconst InfoCardMedia = ({\n  media = [],\n  className,\n  loading = undefined,\n  shrinkHeight = 75,\n  expandHeight = 150,\n}: InfoCardMediaProps) => {\n  const { isHovered } = useContext(InfoCardContext);\n  const { setAllImagesLoaded } = useContext(InfoCardImageContext);\n  const [isOverflowVisible, setIsOverflowVisible] = useState(false);\n  const loadedMedia = useRef(new Set());\n\n  const handleMediaLoad = (mediaSrc: string) => {\n    loadedMedia.current.add(mediaSrc);\n    if (loadedMedia.current.size === Math.min(3, media.slice(0, 3).length)) {\n      setAllImagesLoaded(true);\n    }\n  };\n\n  const processedMedia = useMemo(\n    () =>\n      media.map((item) => ({\n        ...item,\n        type: item.type || \"image\",\n      })),\n    [media]\n  );\n\n  const displayMedia = useMemo(\n    () => processedMedia.slice(0, 3),\n    [processedMedia]\n  );\n\n  useEffect(() => {\n    if (media.length > 0) {\n      setAllImagesLoaded(false);\n      loadedMedia.current.clear();\n    } else {\n      setAllImagesLoaded(true); // No media to load\n    }\n  }, [media.length]);\n\n  useEffect(() => {\n    let timeoutId: NodeJS.Timeout;\n    if (isHovered) {\n      timeoutId = setTimeout(() => {\n        setIsOverflowVisible(true);\n      }, 100);\n    } else {\n      setIsOverflowVisible(false);\n    }\n    return () => clearTimeout(timeoutId);\n  }, [isHovered]);\n\n  const mediaCount = displayMedia.length;\n\n  const getRotation = (index: number) => {\n    if (!isHovered || mediaCount === 1) return 0;\n    return (index - (mediaCount === 2 ? 0.5 : 1)) * 5;\n  };\n\n  const getTranslateX = (index: number) => {\n    if (!isHovered || mediaCount === 1) return 0;\n    return (index - (mediaCount === 2 ? 0.5 : 1)) * 20;\n  };\n\n  const getTranslateY = (index: number) => {\n    if (!isHovered) return 0;\n    if (mediaCount === 1) return -5;\n    return index === 0 ? -10 : index === 1 ? -5 : 0;\n  };\n\n  const getScale = (index: number) => {\n    if (!isHovered) return 1;\n    return mediaCount === 1 ? 1 : 0.95 + index * 0.02;\n  };\n\n  return (\n    <InfoCardImageContext.Provider\n      value={{\n        handleMediaLoad,\n        setAllImagesLoaded,\n      }}\n    >\n      <motion.div\n        className={cn(\"relative mt-2 rounded-md\", className)}\n        animate={{\n          height:\n            media.length > 0\n              ? isHovered\n                ? expandHeight\n                : shrinkHeight\n              : \"auto\",\n        }}\n        style={{\n          overflow: isOverflowVisible ? \"visible\" : \"hidden\",\n        }}\n        transition={{\n          type: \"spring\",\n          stiffness: 300,\n          damping: 30,\n          duration: 0.3,\n        }}\n      >\n        <div\n          className={cn(\n            \"relative\",\n            media.length > 0 ? { height: shrinkHeight } : \"h-auto\"\n          )}\n        >\n          {displayMedia.map((item, index) => {\n            const {\n              type,\n              src,\n              alt,\n              className: itemClassName,\n              ...mediaProps\n            } = item;\n\n            return (\n              <motion.div\n                key={src}\n                className=\"absolute w-full\"\n                animate={{\n                  rotateZ: getRotation(index),\n                  x: getTranslateX(index),\n                  y: getTranslateY(index),\n                  scale: getScale(index),\n                }}\n                transition={{\n                  type: \"spring\",\n                  stiffness: 300,\n                  damping: 30,\n                }}\n              >\n                {type === \"video\" ? (\n                  <video\n                    src={src}\n                    className={cn(\n                      \"w-full rounded-md border border-gray-200 dark:border-zinc-700/10 object-cover shadow-lg\",\n                      itemClassName\n                    )}\n                    onLoadedData={() => handleMediaLoad(src)}\n                    preload=\"metadata\"\n                    muted\n                    playsInline\n                    {...mediaProps}\n                  />\n                ) : (\n                  <img\n                    src={src}\n                    alt={alt}\n                    className={cn(\n                      \"w-full rounded-md border border-gray-200 dark:border-zinc-700/10 object-cover shadow-lg\",\n                      itemClassName\n                    )}\n                    onLoad={() => handleMediaLoad(src)}\n                    loading={loading}\n                    {...mediaProps}\n                  />\n                )}\n              </motion.div>\n            );\n          })}\n        </div>\n\n        <motion.div\n          className=\"absolute right-0 bottom-0 left-0 h-10 bg-gradient-to-b from-transparent to-white dark:to-zinc-900\"\n          animate={{ opacity: isHovered ? 0 : 1 }}\n          transition={{\n            type: \"spring\",\n            stiffness: 300,\n            damping: 30,\n            duration: 0.3,\n          }}\n        />\n      </motion.div>\n    </InfoCardImageContext.Provider>\n  );\n};\n\nexport {\n  InfoCard,\n  InfoCardTitle,\n  InfoCardDescription,\n  InfoCardContent,\n  InfoCardMedia,\n  InfoCardFooter,\n  InfoCardDismiss,\n  InfoCardAction,\n};\n",
      "type": "registry:component",
      "target": "components/kl-ui/info-card.tsx"
    }
  ]
}