{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"post-swiper","type":"registry:component","title":"Post Swiper","description":"A deck of social posts you swipe through, card by card.","author":"DiMaac (HankTheTank) <https://dimaac.com>","dependencies":["swiper","lucide-react"],"files":[{"path":"registry/dimaac/post-swiper/post-swiper.tsx","type":"registry:component","target":"components/dimaac/post-swiper.tsx","content":"\"use client\";\n\nimport { useEffect, useState, type CSSProperties, type ReactNode } from \"react\";\nimport { Swiper, SwiperSlide } from \"swiper/react\";\nimport { A11y, Autoplay, EffectCards, Keyboard } from \"swiper/modules\";\nimport type { Swiper as SwiperInstance } from \"swiper\";\nimport { ArrowLeft, ArrowRight, BadgeCheck, Bookmark, ChartNoAxesColumn, Ellipsis, Globe, Heart, MessageCircle, Repeat2, Send, Share, ThumbsUp } from \"lucide-react\";\nimport \"swiper/css\";\nimport \"swiper/css/effect-cards\";\nimport \"swiper/css/a11y\";\n\n// A deck of social posts you swipe through. Swiper's \"cards\" effect does the stacking and the\n// drag; the posts are three small card looks (a short-text post, a photo-first post and a\n// feed post with actions) so one deck can mix them. Each card has working like and save buttons.\n\nexport type SocialPost = {\n  /** A stable key for the post. */\n  id: string;\n  /** Which card look to use: \"x\" (short text post), \"facebook\" (feed post) or \"instagram\" (photo post). */\n  platform: \"x\" | \"facebook\" | \"instagram\";\n  /** The author's display name. */\n  name: string;\n  /** The author's handle without the @ (used by the \"x\" look). */\n  handle?: string;\n  /** URL of the author's round profile photo. */\n  avatar: string;\n  /** When it was posted, as short text: \"1h\", \"Yesterday\". */\n  time: string;\n  /** Shows the verified badge next to the name. */\n  verified?: boolean;\n  /** The post text. */\n  text?: string;\n  /** URL of the post's photo. It fills the space the text leaves. */\n  image?: string;\n  /** Describes the photo for screen readers. */\n  imageAlt?: string;\n  /** Your own picture area in place of `image`: a quote card, a chart, anything. It fills the same space. */\n  media?: ReactNode;\n  /** The like count. The like button adds one. */\n  likes?: number;\n  /** The comment count. */\n  comments?: number;\n  /** The share or repost count. */\n  shares?: number;\n};\n\nexport type PostSwiperProps = {\n  /** The posts, front card first. Four to ten work best. */\n  posts: SocialPost[];\n  /** Wraps around: the last card is followed by the first. Needs at least three posts. */\n  loop?: boolean;\n  /** Shows a grab cursor over the deck. */\n  grabCursor?: boolean;\n  /** Seconds between automatic swipes. 0 turns autoplay off. It pauses while the pointer is over the deck. */\n  autoplayDelay?: number;\n  /** How much each card behind the front one is turned, in degrees. */\n  tilt?: number;\n  /** How far each card behind the front one sits out from it, in pixels. */\n  spread?: number;\n  /** Darkens the cards behind the front one, for depth. */\n  shadows?: boolean;\n  /** Card colours: \"dark\" or \"light\". */\n  appearance?: \"dark\" | \"light\";\n  /** The card width in pixels. It shrinks to fit a narrow screen. */\n  cardWidth?: number;\n  /** Card width divided by card height. Lower is taller. */\n  cardRatio?: number;\n  /** Shows the arrow buttons and the counter under the deck. */\n  showNav?: boolean;\n  /** The name screen readers give the deck. */\n  label?: string;\n  /** Extra classes for the outer box. */\n  className?: string;\n  /** Extra classes for each card, e.g. a different radius. */\n  cardClassName?: string;\n};\n\n// Colours of the two card looks. The card reads them as CSS variables, so a change here is the only edit.\nconst palettes: Record<NonNullable<PostSwiperProps[\"appearance\"]>, CSSProperties> = {\n  dark: { \"--pc-bg\": \"#202224\", \"--pc-text\": \"#f1f2f3\", \"--pc-muted\": \"#9ea3a9\", \"--pc-line\": \"rgba(255,255,255,0.1)\", \"--pc-hover\": \"rgba(255,255,255,0.07)\" } as CSSProperties,\n  light: { \"--pc-bg\": \"#ffffff\", \"--pc-text\": \"#14171a\", \"--pc-muted\": \"#667078\", \"--pc-line\": \"rgba(20,23,26,0.12)\", \"--pc-hover\": \"rgba(20,23,26,0.06)\" } as CSSProperties,\n};\n\nconst compact = new Intl.NumberFormat(\"en\", { notation: \"compact\", maximumFractionDigits: 1 });\nconst full = new Intl.NumberFormat(\"en\");\n\n// A quick scale pop on the icon that was pressed. The Web Animations API cleans itself up and\n// needs no library. Skipped when the visitor prefers reduced motion.\nfunction pop(element: Element | null) {\n  if (!element || typeof element.animate !== \"function\") return;\n  if (window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) return;\n  element.animate([{ transform: \"scale(1)\" }, { transform: \"scale(1.35)\" }, { transform: \"scale(1)\" }], { duration: 320, easing: \"cubic-bezier(0.34, 1.56, 0.64, 1)\" });\n}\n\nexport function PostSwiper({\n  posts,\n  loop = true,\n  grabCursor = true,\n  autoplayDelay = 0,\n  tilt = 2,\n  spread = 10,\n  shadows = true,\n  appearance = \"dark\",\n  cardWidth = 380,\n  cardRatio = 0.74,\n  showNav = true,\n  label = \"Social posts\",\n  className = \"\",\n  cardClassName = \"\",\n}: PostSwiperProps) {\n  const [swiper, setSwiper] = useState<SwiperInstance | null>(null);\n  const [active, setActive] = useState(0);\n  const [reduced, setReduced] = useState(false);\n\n  // Reduced motion is read after mount, so the server and the first client render match\n  useEffect(() => {\n    const query = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    const update = () => setReduced(query.matches);\n    update();\n    query.addEventListener(\"change\", update);\n    return () => query.removeEventListener(\"change\", update);\n  }, []);\n\n  // Swiper reads the card effect options once, so after a change we hand it the new ones and redraw\n  useEffect(() => {\n    if (!swiper || swiper.destroyed) return;\n    Object.assign(swiper.params.cardsEffect ?? {}, { perSlideRotate: tilt, perSlideOffset: spread, slideShadows: shadows });\n    swiper.update();\n  }, [swiper, tilt, spread, shadows]);\n\n  // Autoplay starts and stops from the props (and never runs for visitors who prefer less motion)\n  const autoplayOn = autoplayDelay > 0 && !reduced;\n  useEffect(() => {\n    if (!swiper || swiper.destroyed || !swiper.autoplay) return;\n    if (autoplayOn) {\n      swiper.params.autoplay = { ...(swiper.params.autoplay as object), delay: autoplayDelay * 1000, disableOnInteraction: false, pauseOnMouseEnter: true };\n      swiper.autoplay.start();\n    } else {\n      swiper.autoplay.stop();\n    }\n  }, [swiper, autoplayOn, autoplayDelay]);\n\n  // Only the front card can take focus. The cards behind it (and the loop's duplicates) are inert,\n  // so tabbing never lands on a button you can't see.\n  const markFront = (instance: SwiperInstance) => {\n    instance.slides.forEach((slide) => slide.toggleAttribute(\"inert\", !slide.classList.contains(\"swiper-slide-active\")));\n  };\n\n  const canLoop = loop && posts.length > 2;\n  const go = (direction: 1 | -1) => {\n    if (!swiper || swiper.destroyed) return;\n    if (direction === 1) swiper.slideNext();\n    else swiper.slidePrev();\n  };\n\n  return (\n    <div\n      role=\"region\"\n      aria-roledescription=\"carousel\"\n      aria-label={label}\n      className={`flex w-full flex-col items-center gap-7 ${className}`}\n    >\n      {/* Remounted when loop changes, because Swiper builds its loop copies once */}\n      <Swiper\n        key={String(canLoop)}\n        modules={[EffectCards, Keyboard, A11y, Autoplay]}\n        effect=\"cards\"\n        cardsEffect={{ slideShadows: shadows, perSlideRotate: tilt, perSlideOffset: spread }}\n        grabCursor={grabCursor}\n        loop={canLoop}\n        speed={reduced ? 0 : 500}\n        keyboard={{ enabled: true, onlyInViewport: true }}\n        a11y={{ enabled: true }}\n        autoplay={{ delay: Math.max(autoplayDelay, 1) * 1000, disableOnInteraction: false, pauseOnMouseEnter: true }}\n        onSwiper={(instance) => {\n          setSwiper(instance);\n          setActive(instance.realIndex);\n          markFront(instance);\n          // Autoplay begins on its own; the effect above turns it off when the prop says so\n          if (!autoplayOn) instance.autoplay?.stop();\n        }}\n        onDestroy={() => setSwiper(null)}\n        onRealIndexChange={(instance) => setActive(instance.realIndex)}\n        onSlideChange={markFront}\n        onTransitionEnd={markFront}\n        onSlidesUpdated={markFront}\n        className=\"!overflow-visible\"\n        style={{ width: cardWidth, maxWidth: \"100%\", aspectRatio: String(cardRatio), margin: 0 }}\n      >\n        {posts.map((post) => (\n          <SwiperSlide key={post.id} className={`!rounded-[1.6rem] bg-transparent shadow-[0_30px_60px_-30px_rgba(0,0,0,0.55)] ${cardClassName}`}>\n            <PostCard post={post} appearance={appearance} />\n          </SwiperSlide>\n        ))}\n      </Swiper>\n\n      {showNav && (\n        <div className=\"flex items-center gap-4 font-mono text-[12px] tracking-[0.12em] uppercase tabular-nums\">\n          <NavButton label=\"Previous post\" onClick={() => go(-1)}>\n            <ArrowLeft size={16} aria-hidden=\"true\" />\n          </NavButton>\n          <span aria-hidden=\"true\" className=\"min-w-[5.5ch] text-center opacity-70\">\n            {String(active + 1).padStart(2, \"0\")} / {String(posts.length).padStart(2, \"0\")}\n          </span>\n          <NavButton label=\"Next post\" onClick={() => go(1)}>\n            <ArrowRight size={16} aria-hidden=\"true\" />\n          </NavButton>\n        </div>\n      )}\n    </div>\n  );\n}\n\nfunction NavButton({ label, onClick, children }: { label: string; onClick: () => void; children: ReactNode }) {\n  return (\n    <button\n      type=\"button\"\n      aria-label={label}\n      onClick={onClick}\n      className=\"grid size-10 cursor-pointer place-items-center rounded-full border border-current/25 transition-[background-color,transform] duration-200 hover:bg-current/10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current active:scale-95\"\n    >\n      {children}\n    </button>\n  );\n}\n\n// ---------------------------------------------------------------------------------------------\n// The card. Everything inside is sized in em, and the card sets its own font size from its width\n// (a container query), so the same post reads well at 330px and at 420px.\n// ---------------------------------------------------------------------------------------------\n\nfunction PostCard({ post, appearance }: { post: SocialPost; appearance: NonNullable<PostSwiperProps[\"appearance\"]> }) {\n  return (\n    <div className=\"h-full\" style={{ containerType: \"inline-size\" }}>\n      <article\n        className=\"flex h-full flex-col overflow-hidden rounded-[inherit] border border-[color:var(--pc-line)] bg-[var(--pc-bg)] text-[color:var(--pc-text)] select-none\"\n        style={{ ...palettes[appearance], fontSize: \"clamp(12.5px, 4cqw, 16px)\", fontFamily: '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif' }}\n      >\n        {post.platform === \"x\" && <ShortPost post={post} />}\n        {post.platform === \"facebook\" && <FeedPost post={post} />}\n        {post.platform === \"instagram\" && <PhotoPost post={post} />}\n      </article>\n    </div>\n  );\n}\n\nfunction Avatar({ post, className = \"\" }: { post: SocialPost; className?: string }) {\n  // The name sits right beside it, so the photo is decoration\n  return <img src={post.avatar} alt=\"\" draggable={false} loading=\"lazy\" className={`size-[2.9em] shrink-0 rounded-full object-cover ${className}`} />;\n}\n\nfunction Picture({ post, className = \"\" }: { post: SocialPost; className?: string }) {\n  if (!post.media && !post.image) return null;\n  return (\n    <div className={`relative min-h-0 flex-1 overflow-hidden ${className}`}>\n      {post.media ?? (\n              <img src={post.image} alt={post.imageAlt ?? \"\"} draggable={false} loading=\"lazy\" decoding=\"async\" className=\"absolute inset-0 h-full w-full object-cover\" />\n      )}\n    </div>\n  );\n}\n\nfunction Name({ post }: { post: SocialPost }) {\n  return (\n    <span className=\"flex min-w-0 items-center gap-[0.3em]\">\n      <span className=\"truncate font-bold\">{post.name}</span>\n      {post.verified && <BadgeCheck aria-label=\"Verified\" size=\"1.2em\" strokeWidth={2.2} className=\"shrink-0 fill-[#1d9bf0] text-[var(--pc-bg)]\" />}\n    </span>\n  );\n}\n\n// A small round or pill button. `on` tints it with `colour` and keeps it pressed.\nfunction Action({ label, text, count, on = false, colour, onPress, children, className = \"\" }: { label: string; text?: string; count?: number; on?: boolean; colour?: string; onPress?: (icon: HTMLElement | null) => void; children: ReactNode; className?: string }) {\n  return (\n    <button\n      type=\"button\"\n      aria-label={label}\n      aria-pressed={onPress ? on : undefined}\n      onClick={(event) => onPress?.(event.currentTarget.querySelector<HTMLElement>(\"[data-icon]\"))}\n      style={on && colour ? { color: colour } : undefined}\n      className={`group flex cursor-pointer items-center gap-[0.45em] rounded-full p-[0.5em] text-[color:var(--pc-muted)] transition-colors duration-200 hover:bg-[var(--pc-hover)] hover:text-[color:var(--pc-text)] focus-visible:outline-2 focus-visible:outline-[#1d9bf0] ${className}`}\n    >\n      <span data-icon className=\"grid place-items-center\">\n        {children}\n      </span>\n      {text && <span className=\"text-[0.92em] font-semibold\">{text}</span>}\n      {count !== undefined && <span className=\"text-[0.88em] tabular-nums\">{compact.format(count)}</span>}\n    </button>\n  );\n}\n\n// The short-text look: avatar beside a header, big type when there is no picture, a stats row below.\nfunction ShortPost({ post }: { post: SocialPost }) {\n  const [liked, setLiked] = useState(false);\n  const [reposted, setReposted] = useState(false);\n  const [saved, setSaved] = useState(false);\n  const hasPicture = Boolean(post.image || post.media);\n\n  return (\n    <div className=\"flex h-full flex-col gap-[0.8em] p-[1.1em] pb-[0.6em]\">\n      <header className=\"flex items-center gap-[0.75em]\">\n        <Avatar post={post} />\n        <div className=\"flex min-w-0 flex-1 flex-col leading-tight\">\n          <Name post={post} />\n          <span className=\"truncate text-[0.9em] text-[color:var(--pc-muted)]\">\n            @{post.handle ?? post.name.toLowerCase().replace(/\\s+/g, \"\")} · {post.time}\n          </span>\n        </div>\n        <Ellipsis size=\"1.4em\" aria-hidden=\"true\" className=\"text-[color:var(--pc-muted)]\" />\n      </header>\n\n      {post.text && (\n        <p className={`whitespace-pre-wrap break-words ${hasPicture ? \"text-[1em] leading-[1.45]\" : \"my-auto text-[1.6em] leading-[1.22] font-medium tracking-[-0.015em]\"}`}>{post.text}</p>\n      )}\n      <Picture post={post} className=\"rounded-[1em] border border-[color:var(--pc-line)]\" />\n\n      <div className=\"-mx-[0.5em] mt-auto flex items-center justify-between\">\n        <Action label=\"Reply\" count={post.comments}>\n          <MessageCircle size=\"1.35em\" aria-hidden=\"true\" />\n        </Action>\n        <Action label=\"Repost\" count={(post.shares ?? 0) + (reposted ? 1 : 0)} on={reposted} colour=\"#00ba7c\" onPress={(icon) => { setReposted(!reposted); if (!reposted) pop(icon); }}>\n          <Repeat2 size=\"1.4em\" aria-hidden=\"true\" />\n        </Action>\n        <Action label=\"Like\" count={(post.likes ?? 0) + (liked ? 1 : 0)} on={liked} colour=\"#f91880\" onPress={(icon) => { setLiked(!liked); if (!liked) pop(icon); }}>\n          <Heart size=\"1.3em\" aria-hidden=\"true\" fill={liked ? \"currentColor\" : \"none\"} />\n        </Action>\n        <Action label=\"Views\">\n          <ChartNoAxesColumn size=\"1.35em\" aria-hidden=\"true\" />\n        </Action>\n        <Action label=\"Save\" on={saved} colour=\"#1d9bf0\" onPress={(icon) => { setSaved(!saved); if (!saved) pop(icon); }}>\n          <Bookmark size=\"1.3em\" aria-hidden=\"true\" fill={saved ? \"currentColor\" : \"none\"} />\n        </Action>\n      </div>\n    </div>\n  );\n}\n\n// The feed look: header row, text, a full-width picture, a reactions line and three wide buttons.\nfunction FeedPost({ post }: { post: SocialPost }) {\n  const [liked, setLiked] = useState(false);\n  const hasPicture = Boolean(post.image || post.media);\n  const likes = (post.likes ?? 0) + (liked ? 1 : 0);\n\n  return (\n    <div className=\"flex h-full flex-col\">\n      <header className=\"flex items-center gap-[0.7em] p-[0.9em] pb-[0.7em]\">\n        <Avatar post={post} className=\"size-[2.7em]\" />\n        <div className=\"flex min-w-0 flex-1 flex-col leading-tight\">\n          <Name post={post} />\n          <span className=\"flex items-center gap-[0.35em] text-[0.82em] text-[color:var(--pc-muted)]\">\n            {post.time} · <Globe size=\"1em\" aria-label=\"Public\" />\n          </span>\n        </div>\n        <Ellipsis size=\"1.4em\" aria-hidden=\"true\" className=\"text-[color:var(--pc-muted)]\" />\n      </header>\n\n      {post.text && (\n        <p className={`px-[0.95em] break-words whitespace-pre-wrap ${hasPicture ? \"pb-[0.8em] text-[0.97em] leading-[1.45]\" : \"my-auto pb-[0.8em] text-[1.55em] leading-[1.25] font-medium tracking-[-0.015em]\"}`}>{post.text}</p>\n      )}\n      <Picture post={post} />\n\n      <div className=\"flex items-center justify-between px-[0.95em] pt-[0.7em] pb-[0.5em] text-[0.86em] text-[color:var(--pc-muted)]\">\n        <span className=\"flex items-center gap-[0.5em]\">\n          <span className=\"grid size-[1.7em] place-items-center rounded-full bg-[#1877f2] text-white\">\n            <ThumbsUp size=\"0.95em\" fill=\"currentColor\" aria-hidden=\"true\" />\n          </span>\n          <span className=\"tabular-nums\">{full.format(likes)}</span>\n        </span>\n        <span className=\"tabular-nums\">\n          {full.format(post.comments ?? 0)} comments · {full.format(post.shares ?? 0)} shares\n        </span>\n      </div>\n\n      <div className=\"mx-[0.95em] mb-[0.5em] grid grid-cols-3 gap-[0.3em] border-t border-[color:var(--pc-line)] pt-[0.4em]\">\n        <Action label=\"Like\" text=\"Like\" on={liked} colour=\"#1877f2\" onPress={(icon) => { setLiked(!liked); if (!liked) pop(icon); }} className=\"justify-center rounded-[0.6em]\">\n          <ThumbsUp size=\"1.25em\" aria-hidden=\"true\" fill={liked ? \"currentColor\" : \"none\"} />\n        </Action>\n        <Action label=\"Comment\" text=\"Comment\" className=\"justify-center rounded-[0.6em]\">\n          <MessageCircle size=\"1.25em\" aria-hidden=\"true\" />\n        </Action>\n        <Action label=\"Share\" text=\"Share\" className=\"justify-center rounded-[0.6em]\">\n          <Share size=\"1.25em\" aria-hidden=\"true\" />\n        </Action>\n      </div>\n    </div>\n  );\n}\n\n// The photo look: a ringed avatar, the picture taking most of the card, actions and a caption.\nfunction PhotoPost({ post }: { post: SocialPost }) {\n  const [liked, setLiked] = useState(false);\n  const [saved, setSaved] = useState(false);\n  const likes = (post.likes ?? 0) + (liked ? 1 : 0);\n\n  return (\n    <div className=\"flex h-full flex-col\">\n      <header className=\"flex items-center gap-[0.7em] p-[0.8em]\">\n        {/* The gradient ring is the avatar's padding showing through */}\n        <span className=\"rounded-full bg-[conic-gradient(from_200deg,#ffcb03,#e6683c,#dc2743,#cc2366,#d300c5,#ffcb03)] p-[2px]\">\n          <Avatar post={post} className=\"size-[2.5em] border-2 border-[var(--pc-bg)]\" />\n        </span>\n        <div className=\"flex min-w-0 flex-1 items-center gap-[0.4em] text-[0.95em] leading-tight\">\n          <Name post={post} />\n          <span className=\"text-[color:var(--pc-muted)]\">· {post.time}</span>\n        </div>\n        <Ellipsis size=\"1.4em\" aria-hidden=\"true\" className=\"text-[color:var(--pc-muted)]\" />\n      </header>\n\n      <Picture post={post} />\n\n      <div className=\"flex items-center gap-[0.1em] px-[0.5em] pt-[0.45em]\">\n        <Action label=\"Like\" on={liked} colour=\"#ef4444\" onPress={(icon) => { setLiked(!liked); if (!liked) pop(icon); }} className=\"text-[color:var(--pc-text)]\">\n          <Heart size=\"1.65em\" aria-hidden=\"true\" fill={liked ? \"currentColor\" : \"none\"} />\n        </Action>\n        <Action label=\"Comment\" className=\"text-[color:var(--pc-text)]\">\n          <MessageCircle size=\"1.55em\" aria-hidden=\"true\" />\n        </Action>\n        <Action label=\"Send\" className=\"text-[color:var(--pc-text)]\">\n          <Send size=\"1.5em\" aria-hidden=\"true\" />\n        </Action>\n        <Action label=\"Save\" on={saved} onPress={(icon) => { setSaved(!saved); if (!saved) pop(icon); }} className=\"ml-auto text-[color:var(--pc-text)]\">\n          <Bookmark size=\"1.55em\" aria-hidden=\"true\" fill={saved ? \"currentColor\" : \"none\"} />\n        </Action>\n      </div>\n\n      <div className=\"px-[0.95em] pt-[0.1em] pb-[0.9em] text-[0.93em] leading-[1.4]\">\n        <p className=\"font-bold tabular-nums\">{full.format(likes)} likes</p>\n        {post.text && (\n          <p className=\"mt-[0.15em] line-clamp-2\">\n            <span className=\"font-bold\">{post.name}</span> {post.text}\n          </p>\n        )}\n      </div>\n    </div>\n  );\n}\n"}],"categories":["cards","swiper","social","posts","stack","drag"],"docs":"import { PostSwiper } from \"@/components/dimaac/post-swiper\". Give its parent a size: it fills it. Docs, playground and props: https://ui.dimaac.com/components/post-swiper","meta":{"tier":"free","preview":"https://ui.dimaac.com/components/post-swiper","credits":[{"by":"Benginur Hajjaj","url":"https://www.lummi.ai/photo/calm-portrait-d1zqo","what":"Photo","source":"Lummi","licence":"Lummi License"},{"by":"rena","url":"https://www.lummi.ai/photo/stylish-portrait-upjeu","what":"Photo","source":"Lummi","licence":"Lummi License"},{"by":"Amino","url":"https://www.lummi.ai/photo/vivid-portrait-on-pink-oxbow","what":"Photo","source":"Lummi","licence":"Lummi License"},{"by":"Steph Meade","url":"https://www.lummi.ai/photo/expressive-minimal-portrait-jrfyc","what":"Photo","source":"Lummi","licence":"Lummi License"},{"by":"Lloyd Creates","url":"https://www.lummi.ai/photo/poised-portrait-ooi9b","what":"Photo","source":"Lummi","licence":"Lummi License"},{"by":"Amino","url":"https://www.lummi.ai/photo/stylish-portrait-8inth","what":"Photo","source":"Lummi","licence":"Lummi License"},{"by":"Viri Gutiérrez","url":"https://www.lummi.ai/photo/contemplative-silhouette-against-modern-minimalist-architecture-1-haa","what":"Photo","source":"Lummi","licence":"Lummi License"},{"by":"Beth Design Studio","url":"https://www.lummi.ai/photo/desert-wanderer-9puhu","what":"Photo","source":"Lummi","licence":"Lummi License"},{"by":"Dona Mara","url":"https://www.lummi.ai/photo/elegant-perfume-bottle-j5t5p","what":"Photo","source":"Lummi","licence":"Lummi License"},{"by":"Ricardo Matos","url":"https://www.lummi.ai/photo/surreal-3d-planet-scene-ofhhw","what":"Photo","source":"Lummi","licence":"Lummi License"},{"by":"Krystyna","url":"https://www.lummi.ai/photo/moody-solitary-figure-x1avl","what":"Photo","source":"Lummi","licence":"Lummi License"}]}}