{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tweet",
  "title": "Tweet",
  "description": "Embed Twitter/X posts with custom styling.",
  "dependencies": [
    "react-tweet",
    "@icons-pack/react-simple-icons"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/spell-ui/tweet.tsx",
      "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport {\n  enrichTweet,\n  useTweet,\n  type EnrichedTweet,\n} from \"react-tweet\";\nimport { cn } from \"@/lib/utils\";\nimport { Check, Link2 } from \"lucide-react\";\nimport { SiX } from \"@icons-pack/react-simple-icons\";\n\nconst VerifiedBadge = ({ className }: { className?: string }) => (\n  <svg\n    viewBox=\"0 0 22 22\"\n    className={className}\n    fill=\"currentColor\"\n    aria-label=\"Verified account\"\n    role=\"img\"\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-.272-1.902-.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.072 2.072 4.4-4.794 1.347 1.246z\" />\n  </svg>\n);\n\nconst formatNumber = (num: number): string => {\n  if (num >= 1000000) {\n    return (num / 1000000).toFixed(1).replace(/\\.0$/, \"\") + \"M\";\n  }\n  if (num >= 1000) {\n    return (num / 1000).toFixed(1).replace(/\\.0$/, \"\") + \"k\";\n  }\n  return num.toString();\n};\n\nconst formatDate = (dateString: string): string => {\n  const date = new Date(dateString);\n  const hours = date.getHours();\n  const minutes = date.getMinutes();\n  const ampm = hours >= 12 ? \"PM\" : \"AM\";\n  const hour12 = hours % 12 || 12;\n  const monthNames = [\n    \"Jan\",\n    \"Feb\",\n    \"Mar\",\n    \"Apr\",\n    \"May\",\n    \"Jun\",\n    \"Jul\",\n    \"Aug\",\n    \"Sep\",\n    \"Oct\",\n    \"Nov\",\n    \"Dec\",\n  ];\n  const month = monthNames[date.getMonth()];\n  const day = date.getDate();\n  const year = date.getFullYear();\n\n  return `${hour12}:${minutes.toString().padStart(2, \"0\")} ${ampm} · ${month} ${day}, ${year}`;\n};\n\nconst TweetSkeleton = ({ className }: { className?: string }) => (\n  <div\n    className={cn(\n      \"block w-full max-w-[590px] rounded-xl p-4 dark:border dark:border-muted not-dark:shadow-[0_0_0_1px_rgba(0,0,0,.08),_0px_2px_2px_rgba(0,0,0,.04)]\",\n      className\n    )}\n  >\n    <div className=\"flex items-center gap-2\">\n      <div className=\"size-[38px] shrink-0 animate-pulse rounded-full bg-muted\" />\n      <div className=\"flex flex-col gap-1\">\n        <div className=\"h-4 w-24 animate-pulse rounded bg-muted\" />\n        <div className=\"h-3 w-16 animate-pulse rounded bg-muted\" />\n      </div>\n    </div>\n    <div className=\"mt-4 space-y-2\">\n      <div className=\"h-4 w-full animate-pulse rounded bg-muted\" />\n      <div className=\"h-4 w-3/4 animate-pulse rounded bg-muted\" />\n    </div>\n  </div>\n);\n\nconst TweetNotFound = ({ className }: { className?: string }) => (\n  <div\n    className={cn(\n      \"flex w-full max-w-[590px] flex-col items-center justify-center gap-2 rounded-xl p-6 text-muted-foreground dark:border dark:border-muted not-dark:shadow-[0_0_0_1px_rgba(0,0,0,.08),_0px_2px_2px_rgba(0,0,0,.04)]\",\n      className\n    )}\n  >\n    <p className=\"text-sm\">Tweet not found</p>\n  </div>\n);\n\nconst TweetHeader = ({ tweet }: { tweet: EnrichedTweet }) => (\n  <div className=\"flex items-start justify-between\">\n    <div className=\"flex items-center gap-2\">\n      <img\n        src={tweet.user.profile_image_url_https}\n        alt={tweet.user.name}\n        loading=\"lazy\"\n        width={38}\n        height={38}\n        className=\"rounded-full\"\n      />\n      <div className=\"flex flex-col\">\n        <span className=\"flex items-center gap-1 text-[15px] font-semibold text-primary\">\n          {tweet.user.name}\n          {(tweet.user.verified || tweet.user.is_blue_verified) && (\n            <VerifiedBadge className=\"size-4 text-[#1C9BF1]\" />\n          )}\n        </span>\n        <span className=\"-mt-0.5 text-[13px] text-muted-foreground\">\n          @{tweet.user.screen_name}\n        </span>\n      </div>\n    </div>\n    <a href={tweet.url} target=\"_blank\" rel=\"noopener noreferrer\">\n      <SiX className=\"size-5\" />\n    </a>\n  </div>\n);\n\nconst TweetBody = ({ tweet }: { tweet: EnrichedTweet }) => (\n  <p className=\"mt-3 leading-6 text-primary\">\n    {tweet.entities.map((entity, idx) => {\n      switch (entity.type) {\n        case \"url\":\n        case \"symbol\":\n        case \"hashtag\":\n        case \"mention\":\n          return (\n            <a\n              key={idx}\n              href={entity.href}\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"text-[#1C9BF1] hover:underline\"\n            >\n              {entity.text}\n            </a>\n          );\n        case \"text\":\n          return (\n            <span\n              key={idx}\n              dangerouslySetInnerHTML={{ __html: entity.text }}\n            />\n          );\n        default:\n          return null;\n      }\n    })}\n  </p>\n);\n\nconst TweetMedia = ({ tweet }: { tweet: EnrichedTweet }) => {\n  if (!tweet.video && !tweet.photos) return null;\n\n  const getVideoSource = () => {\n    if (!tweet.video?.variants) return null;\n\n    const getResolution = (url: string): number => {\n      const match = url.match(/\\/(\\d+)x(\\d+)\\//);\n      if (match) {\n        return parseInt(match[1]) * parseInt(match[2]);\n      }\n      return 0;\n    };\n\n    const mp4Variants = tweet.video.variants\n      .filter((v) => v.type === \"video/mp4\")\n      .sort((a, b) => getResolution(b.src) - getResolution(a.src));\n\n    if (mp4Variants.length > 0) {\n      return { src: mp4Variants[0].src, type: \"video/mp4\" };\n    }\n\n    const firstVariant = tweet.video.variants[0];\n    return { src: firstVariant.src, type: firstVariant.type };\n  };\n\n  const videoSource = getVideoSource();\n\n  return (\n    <div className=\"mt-4\">\n      {tweet.video && videoSource && (\n        <video\n          poster={tweet.video.poster}\n          autoPlay\n          loop\n          muted\n          playsInline\n          className=\"w-full rounded-lg\"\n        >\n          <source src={videoSource.src} type={videoSource.type} />\n        </video>\n      )}\n      {tweet.photos && (\n        <div\n          className={cn(\n            \"grid gap-1\",\n            tweet.photos.length === 1 && \"grid-cols-1\",\n            tweet.photos.length === 2 && \"grid-cols-2\",\n            tweet.photos.length >= 3 && \"grid-cols-2\"\n          )}\n        >\n          {tweet.photos.map((photo, idx) => (\n            <img\n              key={photo.url}\n              src={photo.url}\n              alt={`Photo ${idx + 1}`}\n              loading=\"lazy\"\n              className={cn(\n                \"w-full rounded-lg object-cover\",\n                tweet.photos &&\n                  tweet.photos.length === 3 &&\n                  idx === 0 &&\n                  \"row-span-2\"\n              )}\n            />\n          ))}\n        </div>\n      )}\n    </div>\n  );\n};\n\ninterface TweetFooterProps {\n  tweet: EnrichedTweet;\n  showDate?: boolean;\n  showLikeButton?: boolean;\n  showCopyLink?: boolean;\n}\n\nconst TweetFooter = ({\n  tweet,\n  showDate = true,\n  showLikeButton = true,\n  showCopyLink = true,\n}: TweetFooterProps) => {\n  const [isCopied, setIsCopied] = useState(false);\n\n  const handleCopyLink = () => {\n    navigator.clipboard.writeText(tweet.url).catch(() => {});\n    setIsCopied(true);\n    setTimeout(() => setIsCopied(false), 1500);\n  };\n\n  const showActions = showLikeButton || showCopyLink;\n\n  if (!showDate && !showActions) return null;\n\n  return (\n    <>\n      {showDate && (\n        <div className=\"mt-4\">\n          <time\n            className=\"text-sm text-muted-foreground\"\n            dateTime={tweet.created_at}\n          >\n            {formatDate(tweet.created_at)}\n          </time>\n        </div>\n      )}\n      {showActions && (\n        <div className=\"mt-2.5 flex gap-4 border-t border-muted pt-3.5\">\n          {showLikeButton && (\n            <a\n              href={`https://x.com/intent/like?tweet_id=${tweet.id_str}`}\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"flex items-center gap-1.5 text-muted-foreground\"\n            >\n              <svg\n                className=\"text-pink-600\"\n                xmlns=\"http://www.w3.org/2000/svg\"\n                width=\"18\"\n                height=\"18\"\n                viewBox=\"0 0 18 18\"\n              >\n                <g fill=\"currentColor\">\n                  <path\n                    d=\"M12.164,2c-1.195,.015-2.324,.49-3.164,1.306-.84-.815-1.972-1.291-3.178-1.306-2.53,.015-4.582,2.084-4.572,4.609,0,5.253,5.306,8.429,6.932,9.278,.256,.133,.537,.2,.818,.2s.562-.067,.817-.2c1.626-.848,6.933-4.024,6.933-9.275,.009-2.528-2.042-4.597-4.586-4.612Z\"\n                    fill=\"currentColor\"\n                  />\n                </g>\n              </svg>\n              <span className=\"text-sm text-medium transition-colors hover:text-pink-600\">\n                {formatNumber(tweet.favorite_count)}\n              </span>\n            </a>\n          )}\n          {showCopyLink && (\n            <button\n              onClick={handleCopyLink}\n              className=\"flex cursor-pointer items-center gap-1.5 text-muted-foreground\"\n            >\n              {isCopied ? (\n                <Check className=\"size-4 text-emerald-500\" />\n              ) : (\n                <Link2 className=\"size-4\" />\n              )}\n              <span className=\"text-sm text-medium\">Copy link</span>\n            </button>\n          )}\n        </div>\n      )}\n    </>\n  );\n};\n\ninterface TweetContentProps {\n  tweet: EnrichedTweet;\n  className?: string;\n  showDate?: boolean;\n  showLikeButton?: boolean;\n  showCopyLink?: boolean;\n}\n\nconst TweetContent = ({\n  tweet,\n  className,\n  showDate,\n  showLikeButton,\n  showCopyLink,\n}: TweetContentProps) => {\n  return (\n    <div\n      className={cn(\n        \"w-full max-w-[590px] rounded-xl p-4 dark:border dark:border-muted not-dark:shadow-[0_0_0_1px_rgba(0,0,0,.08),_0px_2px_2px_rgba(0,0,0,.04)]\",\n        className\n      )}\n    >\n      <TweetHeader tweet={tweet} />\n      <TweetBody tweet={tweet} />\n      <TweetMedia tweet={tweet} />\n      <TweetFooter\n        tweet={tweet}\n        showDate={showDate}\n        showLikeButton={showLikeButton}\n        showCopyLink={showCopyLink}\n      />\n    </div>\n  );\n};\n\ninterface TweetProps {\n  id: string;\n  className?: string;\n  showDate?: boolean;\n  showLikeButton?: boolean;\n  showCopyLink?: boolean;\n}\n\nexport function Tweet({\n  id,\n  className,\n  showDate = true,\n  showLikeButton = true,\n  showCopyLink = true,\n}: TweetProps) {\n  const { data: tweet, isLoading, error } = useTweet(id);\n\n  if (isLoading) {\n    return <TweetSkeleton className={className} />;\n  }\n\n  if (error || !tweet) {\n    return <TweetNotFound className={className} />;\n  }\n\n  // X's syndication API omits empty entity arrays, but react-tweet's\n  // enrichTweet assumes they exist and throws \"n is not iterable\" otherwise.\n  let enrichedTweet: EnrichedTweet;\n  try {\n    const entities = tweet.entities ?? {};\n    enrichedTweet = enrichTweet({\n      ...tweet,\n      entities: {\n        ...entities,\n        hashtags: entities.hashtags ?? [],\n        urls: entities.urls ?? [],\n        user_mentions: entities.user_mentions ?? [],\n        symbols: entities.symbols ?? [],\n      },\n    });\n  } catch {\n    return <TweetNotFound className={className} />;\n  }\n\n  return (\n    <TweetContent\n      tweet={enrichedTweet}\n      className={className}\n      showDate={showDate}\n      showLikeButton={showLikeButton}\n      showCopyLink={showCopyLink}\n    />\n  );\n}\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}