{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["react-tweet"],"description":"A beautiful tweet card component for displaying Twitter/X posts.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { type TweetProps, useTweet } from \"react-tweet\";\n\nimport { SmoothTweet, TweetNotFound, TweetSkeleton } from \"./index\";\n\nexport type ClientTweetCardProps = TweetProps & {\n  className?: string;\n  userInfoPosition?: \"top\" | \"bottom\";\n  avatarRounded?: string;\n};\n\nexport const ClientTweetCard = ({\n  id,\n  apiUrl,\n  fallback = <TweetSkeleton />,\n  components,\n  fetchOptions,\n  onError,\n  ...props\n}: ClientTweetCardProps) => {\n  const { data, error, isLoading } = useTweet(id, apiUrl, fetchOptions);\n\n  if (isLoading) {\n    return fallback;\n  }\n  if (error || !data) {\n    const NotFound = components?.TweetNotFound || TweetNotFound;\n    return <NotFound error={onError ? onError(error) : error} />;\n  }\n\n  return <SmoothTweet tweet={data} {...props} />;\n};\n","path":"client.tsx","target":"components/smoothui/tweet-card/client.tsx","type":"registry:ui"},{"content":"/* eslint-disable @next/next/no-img-element */\n\nimport { cn } from \"@/lib/utils\";\nimport { Suspense } from \"react\";\nimport { type EnrichedTweet, enrichTweet, type TweetProps } from \"react-tweet\";\nimport { getTweet, type Tweet } from \"react-tweet/api\";\n\ninterface IconProps {\n  className?: string;\n  [key: string]: unknown;\n}\n\nconst ExternalLink = ({ className, ...props }: IconProps) => (\n  <svg\n    aria-hidden=\"true\"\n    className={className}\n    fill=\"none\"\n    height=\"16\"\n    stroke=\"currentColor\"\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n    strokeWidth=\"2\"\n    viewBox=\"0 0 24 24\"\n    width=\"16\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    {...props}\n  >\n    <path d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\" />\n    <polyline points=\"15 3 21 3 21 9\" />\n    <line x1=\"10\" x2=\"21\" y1=\"14\" y2=\"3\" />\n  </svg>\n);\n\nexport const truncate = (str: string | null, length: number) => {\n  if (!str || str.length <= length) {\n    return str;\n  }\n  return `${str.slice(0, length - 3)}...`;\n};\n\nconst Skeleton = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div className={cn(\"rounded-md bg-muted\", className)} {...props} />\n);\n\nexport const TweetSkeleton = ({\n  className,\n  ...props\n}: {\n  className?: string;\n  [key: string]: unknown;\n}) => (\n  <div\n    className={cn(\n      \"flex size-full max-h-max min-w-72 flex-col gap-2 rounded-xl border p-4\",\n      className\n    )}\n    {...props}\n  >\n    <div className=\"flex flex-row gap-2\">\n      <Skeleton className=\"size-10 shrink-0 rounded-full\" />\n      <Skeleton className=\"h-10 w-full\" />\n    </div>\n    <Skeleton className=\"h-20 w-full\" />\n  </div>\n);\n\nexport const TweetNotFound = ({\n  className,\n  ...props\n}: {\n  className?: string;\n  [key: string]: unknown;\n}) => (\n  <div\n    className={cn(\n      \"flex size-full flex-col items-center justify-center gap-2 rounded-lg border p-4\",\n      className\n    )}\n    {...props}\n  >\n    <h3>Tweet not found</h3>\n  </div>\n);\n\nexport const TweetHeader = ({\n  tweet,\n  avatarRounded = \"rounded\",\n}: {\n  tweet: EnrichedTweet;\n  avatarRounded?: string;\n}) => (\n  <div className=\"flex items-center gap-2\">\n    <a href={tweet.user.url} rel=\"noreferrer\" target=\"_blank\">\n      <img\n        alt={tweet.user.screen_name}\n        className={cn(\"size-10 object-cover object-center\", avatarRounded)}\n        draggable={false}\n        height={40}\n        loading=\"eager\"\n        src={tweet.user.profile_image_url_https}\n        title={`Profile picture of ${tweet.user.name}`}\n        width={40}\n      />\n    </a>\n    <div>\n      <p className=\"font-semibold text-foreground text-sm tracking-tighter 2xl:text-base\">\n        {tweet.user.name}\n      </p>\n      <p className=\"text-foreground/60 text-xs 2xl:text-sm\">\n        @{truncate(tweet.user.screen_name, 16)}\n      </p>\n    </div>\n  </div>\n);\n\nexport const TweetBody = ({ tweet }: { tweet: EnrichedTweet }) => (\n  <blockquote className=\"flex-1\">\n    <p className=\"text-balance text-foreground text-sm tracking-tight 2xl:text-base\">\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                className=\"ease text-foreground transition-colors duration-200 hover:text-foreground/80\"\n                href={entity.href}\n                key={`${entity.type}-${idx}-${entity.text.slice(0, 10)}`}\n                rel=\"noopener noreferrer\"\n                target=\"_blank\"\n              >\n                <span>{entity.text}</span>\n              </a>\n            );\n          case \"text\":\n            return (\n              <span\n                className=\"text-foreground\"\n                // biome-ignore lint/suspicious/noArrayIndexKey: Text entities from tweet API have no unique IDs\n                key={`text-${idx}`}\n              >\n                {entity.text}\n              </span>\n            );\n          default:\n            return null;\n        }\n      })}\n    </p>\n  </blockquote>\n);\n\nexport const TweetMedia = ({ tweet }: { tweet: EnrichedTweet }) => {\n  const hasVideo =\n    tweet.video &&\n    Array.isArray(tweet.video.variants) &&\n    tweet.video.variants.length > 0;\n  const hasPhotos = tweet.photos && tweet.photos.length > 0;\n  const hasCardThumbnail =\n    // @ts-expect-error package doesn't have type definitions\n    tweet?.card?.binding_values?.thumbnail_image_large?.image_value.url;\n\n  if (!(hasVideo || hasPhotos || hasCardThumbnail)) {\n    return null;\n  }\n\n  return (\n    <div className=\"flex w-full flex-col gap-2\">\n      {hasVideo && tweet.video ? (\n        <video\n          autoPlay\n          className=\"w-full rounded-xl border shadow-sm\"\n          loop\n          muted\n          playsInline\n          poster={tweet.video.poster}\n        >\n          <source src={tweet.video.variants[0].src} type=\"video/mp4\" />\n          Your browser does not support the video tag.\n        </video>\n      ) : null}\n      {hasPhotos && tweet.photos\n        ? (() => {\n            const { photos } = tweet;\n            return (\n              <div className=\"relative grid w-full gap-2\">\n                {photos.length === 1 && (\n                  <img\n                    alt={tweet.text}\n                    className=\"w-full rounded-xl border object-cover shadow-sm\"\n                    draggable={false}\n                    height={photos[0].height}\n                    key={photos[0].url}\n                    src={photos[0].url}\n                    title={`Photo by ${tweet.user.name}`}\n                    width={photos[0].width}\n                  />\n                )}\n                {photos.length === 2 && (\n                  <div className=\"grid grid-cols-2 gap-2\">\n                    {photos.map((photo) => (\n                      <img\n                        alt={tweet.text}\n                        className=\"w-full rounded-xl border object-cover shadow-sm\"\n                        draggable={false}\n                        height={photo.height}\n                        key={photo.url}\n                        src={photo.url}\n                        title={`Photo by ${tweet.user.name}`}\n                        width={photo.width}\n                      />\n                    ))}\n                  </div>\n                )}\n                {photos.length >= 3 && (\n                  <div className=\"grid grid-cols-2 gap-2\">\n                    {photos.slice(0, 4).map((photo, index) => (\n                      <img\n                        alt={tweet.text}\n                        className={cn(\n                          \"w-full rounded-xl border object-cover shadow-sm\",\n                          index === 0 && photos.length > 3 && \"row-span-2\"\n                        )}\n                        draggable={false}\n                        height={photo.height}\n                        key={photo.url}\n                        src={photo.url}\n                        title={`Photo by ${tweet.user.name}`}\n                        width={photo.width}\n                      />\n                    ))}\n                  </div>\n                )}\n              </div>\n            );\n          })()\n        : null}\n      {!(hasVideo || hasPhotos) && hasCardThumbnail && (\n        <img\n          alt={tweet.text}\n          className=\"w-full rounded-xl border object-cover shadow-sm\"\n          draggable={false}\n          src={\n            // @ts-expect-error package doesn't have type definitions\n            tweet.card.binding_values.thumbnail_image_large.image_value.url\n          }\n        />\n      )}\n    </div>\n  );\n};\n\nconst entityList = <T,>(value: T[] | undefined): T[] =>\n  Array.isArray(value) ? value : [];\n\n// react-tweet's enrichTweet walks hashtags, mentions, urls, and symbols\n// with for...of, including on quoted_tweet. The syndication API omits\n// those arrays (entities can be {}), which throws \"entities is not iterable\".\nconst iterableEntities = (\n  entities: Tweet[\"entities\"] | undefined\n): Tweet[\"entities\"] => ({\n  hashtags: entityList(entities?.hashtags),\n  symbols: entityList(entities?.symbols),\n  urls: entityList(entities?.urls),\n  user_mentions: entityList(entities?.user_mentions),\n  ...(Array.isArray(entities?.media) ? { media: entities.media } : {}),\n});\n\nconst withIterableEntities = (tweet: Tweet): Tweet => ({\n  ...tweet,\n  entities: iterableEntities(tweet.entities),\n  ...(tweet.quoted_tweet\n    ? {\n        quoted_tweet: {\n          ...tweet.quoted_tweet,\n          entities: iterableEntities(tweet.quoted_tweet.entities),\n        },\n      }\n    : {}),\n});\n\nexport const SmoothTweet = ({\n  tweet,\n  className,\n  userInfoPosition = \"bottom\",\n  avatarRounded = \"rounded\",\n  ...props\n}: {\n  tweet: Tweet;\n  className?: string;\n  userInfoPosition?: \"top\" | \"bottom\";\n  avatarRounded?: string;\n}) => {\n  const enrichedTweet = enrichTweet(withIterableEntities(tweet));\n  const userInfo = (\n    <TweetHeader avatarRounded={avatarRounded} tweet={enrichedTweet} />\n  );\n  const content = (\n    <div className=\"flex w-full flex-col gap-4\">\n      <TweetMedia tweet={enrichedTweet} />\n      {userInfoPosition === \"bottom\" && userInfo}\n    </div>\n  );\n\n  return (\n    <article\n      className={cn(\n        \"group !p-6 relative flex flex-col items-start justify-between gap-6 overflow-hidden rounded-xl border bg-background lg:gap-8\",\n        className\n      )}\n      data-tweet\n      {...props}\n    >\n      {userInfoPosition === \"top\" && userInfo}\n      <TweetBody tweet={enrichedTweet} />\n      {content}\n      <button\n        aria-label=\"Open tweet in new tab\"\n        className=\"absolute right-6 bottom-6 z-10 flex h-8 w-8 cursor-pointer items-center justify-center rounded-full border bg-primary text-foreground opacity-0 backdrop-blur-sm transition-all duration-200 ease-out group-hover:opacity-100 motion-safe:hover:scale-110\"\n        onClick={() =>\n          window.open(enrichedTweet.url, \"_blank\", \"noopener,noreferrer\")\n        }\n        type=\"button\"\n      >\n        <ExternalLink className=\"h-4 w-4\" />\n      </button>\n    </article>\n  );\n};\n\n/**\n * TweetCard (Server Side Only)\n */\nexport type TweetCardProps = TweetProps & {\n  className?: string;\n  userInfoPosition?: \"top\" | \"bottom\";\n  avatarRounded?: string;\n};\n\nexport const TweetCard = async ({\n  id,\n  components,\n  fallback = <TweetSkeleton />,\n  onError,\n  ...props\n}: TweetCardProps) => {\n  const tweet = id\n    ? await getTweet(id).catch((err) => {\n        if (onError) {\n          onError(err);\n        } else {\n          console.error(err);\n        }\n      })\n    : undefined;\n\n  if (!tweet) {\n    const NotFound = components?.TweetNotFound || TweetNotFound;\n    return <NotFound {...props} />;\n  }\n\n  return (\n    <Suspense fallback={fallback}>\n      <SmoothTweet tweet={tweet} {...props} />\n    </Suspense>\n  );\n};\n\nexport { ClientTweetCard } from \"./client\";\nexport default TweetCard;\n","path":"index.tsx","target":"components/smoothui/tweet-card/index.tsx","type":"registry:ui"}],"name":"tweet-card","registryDependencies":[],"title":"Tweet Card","type":"registry:ui"}