{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "twitter-card",
  "type": "registry:ui",
  "title": "Twitter(X) Card",
  "description": "Interactive Twitter (X) profile card component featuring magnetic 3D tilt, spring physics, and real-time public profile data fetching.",
  "dependencies": [
    "clsx",
    "tailwind-merge",
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/registry/ui/twitter-card.tsx",
      "content": "\"use client\";\n\nimport React, {\n  useCallback,\n  useEffect,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport {\n  motion,\n  useMotionValue,\n  useReducedMotion,\n  useSpring,\n  useTransform,\n} from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface TwitterCardProps {\n  username: string;\n  name?: string;\n  avatarUrl?: string;\n  bannerUrl?: string;\n  staticCard?: boolean;\n  joinedDate?: string;\n  year?: number | string;\n  text?: string;\n  linkText?: string;\n  href?: string;\n  enableLinkTilt?: boolean;\n  linkTiltMaxRotate?: number;\n  enableCardTilt?: boolean;\n  cardTiltMaxRotate?: number;\n  className?: string;\n  popoverClassName?: string;\n  linkClassName?: string;\n  labelClassName?: string;\n}\n\nconst VIEWPORT_MARGIN = 24;\n\nconst XIcon = ({ className }: { className?: string }) => (\n  <svg viewBox=\"0 0 24 24\" className={cn(\"fill-current\", className)}>\n    <path d=\"M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z\" />\n  </svg>\n);\n\nconst VerifiedBadge = ({ className }: { className?: string }) => (\n  <svg\n    viewBox=\"0 0 22 22\"\n    className={cn(\"shrink-0 fill-current\", className)}\n    aria-label=\"Verified account\"\n  >\n    <path d=\"M20.396 11c-.018-.646-.215-1.275-.57-1.816-.354-.54-.852-.972-1.438-1.246.223-.607.27-1.264.14-1.897-.131-.634-.437-1.218-.882-1.687-.47-.445-1.053-.75-1.687-.882-.633-.13-1.29-.083-1.897.14-.273-.587-.704-1.086-1.245-1.44S11.647 1.62 11 1.604c-.646.017-1.273.213-1.813.568s-.969.854-1.24 1.44c-.608-.223-1.267-.27-1.9-.14-.635.13-1.22.436-1.69.882-.445.47-.749 1.055-.878 1.688-.13.633-.08 1.29.144 1.896-.587.274-1.087.705-1.443 1.245-.356.54-.555 1.17-.574 1.817.02.647.218 1.276.574 1.817.356.54.856.972 1.443 1.245-.224.606-.274 1.263-.144 1.896.13.634.433 1.218.877 1.688.47.443 1.054.747 1.687.878.633.132 1.29.084 1.897-.136.274.586.705 1.084 1.246 1.439.54.354 1.17.551 1.816.569.647-.016 1.276-.213 1.817-.567s.972-.854 1.245-1.44c.604.239 1.266.296 1.903.164.636-.132 1.22-.447 1.68-.907.46-.46.776-1.044.908-1.681s.075-1.299-.165-1.903c.586-.274 1.084-.705 1.439-1.246.354-.54.551-1.17.569-1.816zM9.662 14.85l-3.429-3.428 1.293-1.302 2.136 2.136 5.408-5.407 1.293 1.292-6.701 6.709z\" />\n  </svg>\n);\n\nconst LocationIcon = ({ className }: { className?: string }) => (\n  <svg\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    stroke=\"currentColor\"\n    strokeWidth=\"2\"\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n    className={cn(\"shrink-0\", className)}\n  >\n    <path d=\"M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z\" />\n    <circle cx=\"12\" cy=\"10\" r=\"3\" />\n  </svg>\n);\n\nconst LinkIcon = ({ className }: { className?: string }) => (\n  <svg\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    stroke=\"currentColor\"\n    strokeWidth=\"2\"\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n    className={cn(\"shrink-0\", className)}\n  >\n    <path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\" />\n    <path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\" />\n  </svg>\n);\n\nconst CalendarIcon = ({ className }: { className?: string }) => (\n  <svg\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    stroke=\"currentColor\"\n    strokeWidth=\"2\"\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n    className={cn(\"shrink-0\", className)}\n  >\n    <rect width=\"18\" height=\"18\" x=\"3\" y=\"4\" rx=\"2\" ry=\"2\" />\n    <line x1=\"16\" x2=\"16\" y1=\"2\" y2=\"6\" />\n    <line x1=\"8\" x2=\"8\" y1=\"2\" y2=\"6\" />\n    <line x1=\"3\" x2=\"21\" y1=\"10\" y2=\"10\" />\n  </svg>\n);\n\nconst DefaultAvatar = ({ className }: { className?: string }) => (\n  <div\n    className={cn(\n      \"flex h-full w-full items-center justify-center bg-neutral-200 text-neutral-500 dark:bg-neutral-800 dark:text-neutral-400\",\n      className,\n    )}\n  >\n    <svg viewBox=\"0 0 24 24\" className=\"h-8 w-8 fill-current\">\n      <path d=\"M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z\" />\n    </svg>\n  </div>\n);\n\nexport const TwitterCard = ({\n  username,\n  name = \"Twitter User\",\n  avatarUrl,\n  bannerUrl,\n  staticCard = false,\n  joinedDate,\n  year = 2026,\n  text = \"Follow me on\",\n  linkText = \"X\",\n  href,\n  enableLinkTilt = true,\n  linkTiltMaxRotate = 5,\n  enableCardTilt = true,\n  cardTiltMaxRotate = 5,\n  className,\n  popoverClassName,\n  linkClassName,\n  labelClassName,\n}: TwitterCardProps) => {\n  const prefersReducedMotion = useReducedMotion();\n  const [isHovered, setIsHovered] = useState(false);\n  const [failedAvatar, setFailedAvatar] = useState<string | null>(null);\n  const [failedBanner, setFailedBanner] = useState<string | null>(null);\n\n  const profileUrl = href || `https://x.com/${username}`;\n  const linkRef = useRef<HTMLDivElement>(null);\n  const cardRef = useRef<HTMLDivElement>(null);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const tapRevealRef = useRef(false);\n  const [cardShift, setCardShift] = useState(0);\n\n  const [profile, setProfile] = useState({\n    name: name || \"Twitter User\",\n    avatarUrl: avatarUrl || \"\",\n    bannerUrl: bannerUrl || \"\",\n    bio: \"This user hasn't added a bio yet.\",\n    following: 0,\n    followers: 0,\n    joinedDate: joinedDate || `Joined ${year}`,\n    location: \"\",\n    website: null as { url: string; display_url: string } | null,\n    verified: false,\n  });\n\n  useEffect(() => {\n    if (!username) return;\n\n    let isMounted = true;\n    const controller = new AbortController();\n\n    fetch(`https://api.fxtwitter.com/${username}`, {\n      signal: controller.signal,\n    })\n      .then((res) => {\n        if (!res.ok) throw new Error(\"API request failed\");\n        return res.json();\n      })\n      .then((data) => {\n        if (!isMounted) return;\n\n        if (data.code === 200 && data.user) {\n          const user = data.user;\n          const formattedJoined = user.joined\n            ? `Joined ${new Date(user.joined).toLocaleDateString(\"en-US\", {\n                month: \"long\",\n                year: \"numeric\",\n              })}`\n            : joinedDate || `Joined ${year}`;\n\n          setProfile({\n            name: user.name || name || \"Twitter User\",\n            avatarUrl:\n              user.avatar_url?.replace(\"_normal\", \"_400x400\") ||\n              avatarUrl ||\n              \"\",\n            bannerUrl: user.banner_url || bannerUrl || \"\",\n            bio: user.description || \"This user hasn't added a bio yet.\",\n            following: user.following ?? 0,\n            followers: user.followers ?? 0,\n            joinedDate: formattedJoined,\n            location: user.location || \"\",\n            website: user.website || null,\n            verified: Boolean(user.verification?.verified),\n          });\n        }\n      })\n      .catch(() => {});\n\n    return () => {\n      isMounted = false;\n      controller.abort();\n    };\n  }, [username, name, avatarUrl, bannerUrl, joinedDate, year]);\n\n  const linkX = useMotionValue(0);\n  const linkY = useMotionValue(0);\n  const linkSpringX = useSpring(linkX, { stiffness: 320, damping: 24 });\n  const linkSpringY = useSpring(linkY, { stiffness: 320, damping: 24 });\n\n  const linkRotateX = useTransform(\n    linkSpringY,\n    [-1, 1],\n    [linkTiltMaxRotate, -linkTiltMaxRotate],\n  );\n  const linkRotateY = useTransform(\n    linkSpringX,\n    [-1, 1],\n    [-linkTiltMaxRotate, linkTiltMaxRotate],\n  );\n\n  const cardX = useMotionValue(0);\n  const cardY = useMotionValue(0);\n  const cardSpringX = useSpring(cardX, {\n    stiffness: 300,\n    damping: 24,\n    mass: 0.6,\n  });\n  const cardSpringY = useSpring(cardY, {\n    stiffness: 300,\n    damping: 24,\n    mass: 0.6,\n  });\n\n  const cardRotateX = useTransform(\n    cardSpringY,\n    [-1, 1],\n    [cardTiltMaxRotate, -cardTiltMaxRotate],\n  );\n  const cardRotateY = useTransform(\n    cardSpringX,\n    [-1, 1],\n    [-cardTiltMaxRotate, cardTiltMaxRotate],\n  );\n\n  const handleLinkMouseMove = useCallback(\n    (e: React.MouseEvent<HTMLDivElement>) => {\n      if (!enableLinkTilt || !linkRef.current || prefersReducedMotion) return;\n      const rect = linkRef.current.getBoundingClientRect();\n      const nx = (e.clientX - rect.left - rect.width / 2) / (rect.width / 2);\n      const ny = (e.clientY - rect.top - rect.height / 2) / (rect.height / 2);\n      linkX.set(Math.max(-1, Math.min(1, nx)));\n      linkY.set(Math.max(-1, Math.min(1, ny)));\n    },\n    [enableLinkTilt, linkX, linkY, prefersReducedMotion],\n  );\n\n  const handleCardMouseMove = useCallback(\n    (e: React.MouseEvent<HTMLDivElement>) => {\n      if (!enableCardTilt || !cardRef.current || prefersReducedMotion) return;\n      const rect = cardRef.current.getBoundingClientRect();\n      const nx = (e.clientX - rect.left - rect.width / 2) / (rect.width / 2);\n      const ny = (e.clientY - rect.top - rect.height / 2) / (rect.height / 2);\n      cardX.set(Math.max(-1, Math.min(1, nx)));\n      cardY.set(Math.max(-1, Math.min(1, ny)));\n    },\n    [enableCardTilt, cardX, cardY, prefersReducedMotion],\n  );\n\n  const handleContainerMouseLeave = useCallback(() => {\n    setIsHovered(false);\n    linkX.set(0);\n    linkY.set(0);\n    cardX.set(0);\n    cardY.set(0);\n  }, [linkX, linkY, cardX, cardY]);\n\n  useLayoutEffect(() => {\n    if (!isHovered) return;\n    const updateShift = () => {\n      const container = containerRef.current;\n      const card = cardRef.current;\n      if (!container || !card) return;\n      const box = container.getBoundingClientRect();\n      const width = card.offsetWidth;\n      const naturalLeft = box.left + box.width / 2 - width / 2;\n      const maxLeft = window.innerWidth - VIEWPORT_MARGIN - width;\n      const left = Math.max(VIEWPORT_MARGIN, Math.min(maxLeft, naturalLeft));\n      setCardShift(left - naturalLeft);\n    };\n    updateShift();\n    window.addEventListener(\"resize\", updateShift);\n    return () => window.removeEventListener(\"resize\", updateShift);\n  }, [isHovered]);\n\n  useEffect(() => {\n    if (!isHovered) return;\n    const closeOnOutsidePointer = (event: PointerEvent) => {\n      if (!containerRef.current?.contains(event.target as Node)) {\n        handleContainerMouseLeave();\n      }\n    };\n    document.addEventListener(\"pointerdown\", closeOnOutsidePointer);\n    return () =>\n      document.removeEventListener(\"pointerdown\", closeOnOutsidePointer);\n  }, [isHovered, handleContainerMouseLeave]);\n\n  const handleTriggerPointerDown = (e: React.PointerEvent) => {\n    tapRevealRef.current = e.pointerType !== \"mouse\" && !isHovered;\n  };\n\n  const handleTriggerClick = (e: React.MouseEvent) => {\n    if (!tapRevealRef.current) return;\n    tapRevealRef.current = false;\n    e.preventDefault();\n    setIsHovered(true);\n  };\n\n  const handleContainerBlur = (e: React.FocusEvent) => {\n    if (!e.currentTarget.contains(e.relatedTarget as Node | null)) {\n      handleContainerMouseLeave();\n    }\n  };\n\n  const handleContainerKeyDown = (e: React.KeyboardEvent) => {\n    if (e.key === \"Escape\" && isHovered) handleContainerMouseLeave();\n  };\n\n  const handleStaticMouseLeave = useCallback(() => {\n    cardX.set(0);\n    cardY.set(0);\n  }, [cardX, cardY]);\n\n  const formatCount = (count: number | string | undefined): string => {\n    if (count === undefined || count === null) return \"0\";\n    if (typeof count === \"string\") return count;\n    if (count >= 1000000) {\n      return `${(count / 1000000).toFixed(1).replace(/\\.0$/, \"\")}M`;\n    }\n    if (count >= 1000) {\n      return `${(count / 1000).toFixed(1).replace(/\\.0$/, \"\")}K`;\n    }\n    return count.toLocaleString(\"en-US\");\n  };\n\n  const ProfileLink = staticCard ? \"span\" : \"a\";\n\n  const cardContent = (\n    <div className=\"flex flex-col text-left\">\n      <div className=\"relative -mx-4 -mt-4 h-24 overflow-hidden rounded-t-2xl bg-neutral-100 dark:bg-neutral-900\">\n        {profile.bannerUrl && failedBanner !== profile.bannerUrl ? (\n          <img\n            src={profile.bannerUrl}\n            alt=\"Profile Banner\"\n            onError={() => setFailedBanner(profile.bannerUrl)}\n            className=\"h-full w-full object-cover\"\n          />\n        ) : (\n          <div className=\"h-full w-full bg-linear-to-r from-neutral-200 via-neutral-300 to-neutral-200 dark:from-neutral-900 dark:via-neutral-800 dark:to-neutral-900\" />\n        )}\n      </div>\n\n      <div className=\"relative mb-2 flex items-start justify-between\">\n        <div className=\"relative z-10 -mt-8 h-16 w-16 overflow-hidden rounded-full border-4 border-white bg-neutral-100 shadow-md dark:border-neutral-950 dark:bg-neutral-900\">\n          {profile.avatarUrl && failedAvatar !== profile.avatarUrl ? (\n            <img\n              src={profile.avatarUrl}\n              alt={`${profile.name}'s Avatar`}\n              onError={() => setFailedAvatar(profile.avatarUrl)}\n              className=\"h-full w-full object-cover\"\n            />\n          ) : (\n            <DefaultAvatar />\n          )}\n        </div>\n\n        <ProfileLink\n          {...(!staticCard\n            ? {\n                href: profileUrl,\n                target: \"_blank\",\n                rel: \"noopener noreferrer\",\n              }\n            : {})}\n          className=\"mt-2 text-neutral-400 transition-colors hover:text-neutral-900 dark:text-neutral-500 dark:hover:text-neutral-200\"\n          aria-label={`View @${username} on X`}\n        >\n          <XIcon className=\"h-5 w-5\" />\n        </ProfileLink>\n      </div>\n\n      <div className=\"flex flex-col\">\n        <div className=\"flex items-center gap-1.5\">\n          <span className=\"text-base font-semibold leading-snug tracking-tight text-neutral-900 dark:text-white\">\n            {profile.name}\n          </span>\n          {profile.verified && (\n            <VerifiedBadge className=\"h-4 w-4 text-sky-500 dark:text-sky-400\" />\n          )}\n        </div>\n        <ProfileLink\n          {...(!staticCard\n            ? {\n                href: profileUrl,\n                target: \"_blank\",\n                rel: \"noopener noreferrer\",\n              }\n            : {})}\n          className=\"text-sm text-neutral-500 transition-colors hover:text-neutral-800 dark:text-neutral-400 dark:hover:text-neutral-200\"\n        >\n          @{username}\n        </ProfileLink>\n      </div>\n\n      <p className=\"mt-2.5 text-sm leading-relaxed text-neutral-800 dark:text-neutral-200\">\n        {profile.bio}\n      </p>\n\n      <div className=\"mt-2.5 flex flex-wrap gap-x-4 gap-y-1.5 text-xs text-neutral-500 dark:text-neutral-400\">\n        {profile.location && (\n          <div className=\"flex items-center gap-1.5\">\n            <LocationIcon className=\"h-3.5 w-3.5\" />\n            <span>{profile.location}</span>\n          </div>\n        )}\n\n        {profile.website && (\n          <div className=\"flex items-center gap-1.5\">\n            <LinkIcon className=\"h-3.5 w-3.5\" />\n            <ProfileLink\n              {...(!staticCard\n                ? {\n                    href: profile.website.url,\n                    target: \"_blank\",\n                    rel: \"noopener noreferrer\",\n                  }\n                : {})}\n              className=\"text-sky-600 hover:underline dark:text-sky-400\"\n            >\n              {profile.website.display_url}\n            </ProfileLink>\n          </div>\n        )}\n\n        {profile.joinedDate && (\n          <div className=\"flex items-center gap-1.5\">\n            <CalendarIcon className=\"h-3.5 w-3.5\" />\n            <span>{profile.joinedDate}</span>\n          </div>\n        )}\n      </div>\n\n      <div className=\"mt-3 flex items-center gap-4 text-sm\">\n        <div className=\"flex items-center gap-1\">\n          <span className=\"font-semibold text-neutral-900 dark:text-white\">\n            {formatCount(profile.following)}\n          </span>\n          <span className=\"text-neutral-500 dark:text-neutral-400\">\n            Following\n          </span>\n        </div>\n        <div className=\"flex items-center gap-1\">\n          <span className=\"font-semibold text-neutral-900 dark:text-white\">\n            {formatCount(profile.followers)}\n          </span>\n          <span className=\"text-neutral-500 dark:text-neutral-400\">\n            Followers\n          </span>\n        </div>\n      </div>\n    </div>\n  );\n\n  if (staticCard) {\n    return (\n      <div\n        className={cn(\n          \"relative inline-block w-80 perspective-[1000px]\",\n          className,\n        )}\n      >\n        <motion.div\n          ref={cardRef}\n          onMouseMove={handleCardMouseMove}\n          onMouseLeave={handleStaticMouseLeave}\n          style={\n            enableCardTilt && !prefersReducedMotion\n              ? {\n                  rotateX: cardRotateX,\n                  rotateY: cardRotateY,\n                  transformStyle: \"preserve-3d\",\n                }\n              : undefined\n          }\n          className={cn(\n            \"w-80 rounded-2xl border border-dashed border-neutral-300 bg-white/95 p-4 shadow-xl backdrop-blur-md transition-colors dark:border-neutral-800 dark:bg-neutral-950/80\",\n            popoverClassName,\n          )}\n        >\n          {cardContent}\n        </motion.div>\n      </div>\n    );\n  }\n\n  return (\n    <div className={cn(\"inline-flex items-center gap-1.5\", className)}>\n      <span\n        className={cn(\n          \"text-lg font-medium text-neutral-900/60 transition-colors dark:text-neutral-100/60\",\n          labelClassName,\n        )}\n      >\n        {text}\n      </span>\n\n      <div\n        ref={containerRef}\n        className=\"relative inline-flex flex-col items-center perspective-[1000px]\"\n        onMouseEnter={() => setIsHovered(true)}\n        onMouseLeave={handleContainerMouseLeave}\n        onFocus={() => setIsHovered(true)}\n        onBlur={handleContainerBlur}\n        onKeyDown={handleContainerKeyDown}\n      >\n        <motion.div\n          ref={linkRef}\n          onMouseMove={handleLinkMouseMove}\n          style={\n            enableLinkTilt && !prefersReducedMotion\n              ? {\n                  rotateX: linkRotateX,\n                  rotateY: linkRotateY,\n                  transformStyle: \"preserve-3d\",\n                }\n              : undefined\n          }\n          className=\"cursor-pointer\"\n        >\n          <a\n            href={profileUrl}\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            className=\"inline-block\"\n            onPointerDown={handleTriggerPointerDown}\n            onClick={handleTriggerClick}\n          >\n            <span\n              className={cn(\n                \"text-lg font-medium text-neutral-900/60 underline underline-offset-4 transition-colors duration-200 hover:text-neutral-900 dark:text-neutral-100/60 dark:hover:text-neutral-100\",\n                linkClassName,\n              )}\n            >\n              {linkText}\n            </span>\n          </a>\n        </motion.div>\n\n        <div\n          inert={!isHovered}\n          className=\"pointer-events-none absolute bottom-full left-1/2 z-50 mb-3.5\"\n          style={{ transform: `translateX(calc(-50% + ${cardShift}px))` }}\n        >\n          <motion.div\n            ref={cardRef}\n            onMouseMove={handleCardMouseMove}\n            initial=\"hidden\"\n            animate={isHovered ? \"visible\" : \"hidden\"}\n            variants={{\n              hidden: {\n                opacity: 0,\n                y: 8,\n                scale: 0.97,\n                filter: \"blur(2px)\",\n                pointerEvents: \"none\",\n                transition: {\n                  duration: 0.15,\n                  ease: \"easeIn\",\n                },\n              },\n              visible: {\n                opacity: 1,\n                y: 0,\n                scale: 1,\n                filter: \"blur(0px)\",\n                pointerEvents: \"auto\",\n                transition: {\n                  type: \"spring\",\n                  damping: 24,\n                  stiffness: 300,\n                },\n              },\n            }}\n            style={\n              enableCardTilt && !prefersReducedMotion\n                ? {\n                    rotateX: cardRotateX,\n                    rotateY: cardRotateY,\n                    transformStyle: \"preserve-3d\",\n                  }\n                : undefined\n            }\n            className={cn(\n              \"w-80 max-w-[calc(100vw-3rem)] rounded-2xl border border-dashed border-neutral-300 bg-white/95 p-4 shadow-2xl backdrop-blur-xl transition-colors select-text dark:border-neutral-800 dark:bg-neutral-950/85\",\n              \"after:absolute after:top-full after:left-0 after:h-4 after:w-full after:content-['']\",\n              popoverClassName,\n            )}\n          >\n            {cardContent}\n          </motion.div>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default TwitterCard;\n",
      "type": "registry:ui",
      "target": "@ui/twitter-card.tsx"
    }
  ]
}
