{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "hover-media",
  "title": "Hover Media",
  "description": "Inline keyword that reveals a floating image or video that follows the cursor.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/new-york/hover-media/hover-media.tsx",
      "content": "\"use client\"\n\nimport { useState } from \"react\"\nimport { createPortal } from \"react-dom\"\nimport {\n  AnimatePresence,\n  motion,\n  useMotionValue,\n  useReducedMotion,\n  useSpring,\n  useTransform,\n  useVelocity,\n} from \"motion/react\"\nimport { cn } from \"@/lib/utils\"\n\nexport interface HoverMediaProps {\n  /** The keyword(s) that trigger the preview. */\n  children: React.ReactNode\n  /** Image or video URL. */\n  src: string\n  /** Media type. Detected from the file extension when omitted. */\n  type?: \"image\" | \"video\"\n  /** Alt text for images. Default: \"\" */\n  alt?: string\n  /** Preview width in px. Default: 220 */\n  width?: number\n  /** Preview aspect ratio. Default: \"4 / 3\" */\n  aspectRatio?: string\n  /** Maximum tilt in degrees, driven by cursor speed. Default: 12 */\n  tilt?: number\n  /** Distance from the cursor in px. Default: 20 */\n  offset?: number\n  /** Spring stiffness for the follow motion. Default: 300 */\n  stiffness?: number\n  /** Spring damping for the follow motion. Default: 28 */\n  damping?: number\n  className?: string\n  mediaClassName?: string\n}\n\nconst isVideo = (src: string) => /\\.(mp4|webm|ogg|mov)(\\?.*)?$/i.test(src)\n\nexport function HoverMedia({\n  children,\n  src,\n  type,\n  alt = \"\",\n  width = 220,\n  aspectRatio = \"4 / 3\",\n  tilt = 12,\n  offset = 20,\n  stiffness = 300,\n  damping = 28,\n  className,\n  mediaClassName,\n}: HoverMediaProps) {\n  const reduced = useReducedMotion()\n  const [open, setOpen] = useState(false)\n  // Only portal after the first hover so nothing renders during SSR/hydration\n  const [armed, setArmed] = useState(false)\n\n  const spring = { stiffness, damping, mass: 0.4 }\n  const x = useSpring(useMotionValue(0), spring)\n  const y = useSpring(useMotionValue(0), spring)\n  const velocity = useVelocity(x)\n  const rotate = useTransform(velocity, [-1500, 0, 1500], [-tilt, 0, tilt], { clamp: true })\n\n  const video = type ? type === \"video\" : isVideo(src)\n\n  const move = (e: React.PointerEvent) => {\n    x.set(e.clientX + offset)\n    y.set(e.clientY + offset)\n  }\n\n  return (\n    <>\n      <span\n        className={cn(\n          \"cursor-default underline decoration-current/30 decoration-dotted underline-offset-[0.2em] transition-colors hover:decoration-current\",\n          className\n        )}\n        onPointerEnter={(e) => {\n          x.jump(e.clientX + offset)\n          y.jump(e.clientY + offset)\n          setArmed(true)\n          setOpen(true)\n        }}\n        onPointerMove={move}\n        onPointerLeave={() => setOpen(false)}\n      >\n        {children}\n      </span>\n\n      {armed &&\n        createPortal(\n          <AnimatePresence>\n            {open && (\n              <motion.div\n                aria-hidden=\"true\"\n                className=\"pointer-events-none fixed left-0 top-0 z-[100]\"\n                style={{ x, y, rotate: reduced ? 0 : rotate }}\n              >\n                <motion.div\n                  initial={{ opacity: 0, scale: 0.6, filter: \"blur(6px)\" }}\n                  animate={{ opacity: 1, scale: 1, filter: \"blur(0px)\" }}\n                  exit={{ opacity: 0, scale: 0.7, filter: \"blur(6px)\" }}\n                  transition={{ type: \"spring\", stiffness: 400, damping: 30 }}\n                  className={cn(\"overflow-hidden rounded-xl bg-muted shadow-2xl shadow-black/30\", mediaClassName)}\n                  style={{ width, aspectRatio, transformOrigin: \"top left\" }}\n                >\n                  {video ? (\n                    <video src={src} autoPlay muted loop playsInline className=\"size-full object-cover\" />\n                  ) : (\n                    <img src={src} alt={alt} className=\"size-full object-cover\" />\n                  )}\n                </motion.div>\n              </motion.div>\n            )}\n          </AnimatePresence>,\n          document.body\n        )}\n    </>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "docs": "Docs, live playground and every prop: https://trytweenly.vercel.app/docs/components/hover-media",
  "type": "registry:component"
}