Integration Orbit
Concentric rings of integration logos that orbit a featured brand mark on hover.
import type { Block } from 'payload'import { integrationFeaturedMark, integrationFields } from '@/blocks/shared/integrationFields'export const IntegrationOrbit: Block = { slug: 'integrationOrbit', interfaceName: 'IntegrationOrbitBlock', fields: [ // Shared integration core (heading + subtext + integrations). Edit the shared // shape in @/blocks/shared/integrationFields to update every integration variant. ...integrationFields, // Variant-specific: a featured center brand mark the rings orbit around. integrationFeaturedMark, ], labels: { plural: 'Integration Orbit Blocks', singular: 'Integration Orbit', },}import React from 'react'import type { IntegrationOrbitBlock as IntegrationOrbitBlockData } from '@/payload-types'import { Media } from '@/components/Media'import { cn } from '@/utilities/ui'type Props = IntegrationOrbitBlockData & { id?: string className?: string disableInnerContainer?: boolean}/* Place an item around a ring: rotate to its slot, push out by the radius, then counter-rotate the content so the logo stays upright. The animated spin lives in globals.css (.integration-orbit-ring / .integration-orbit-logo) and runs only on hover, so nothing animates on load. */const slotStyle = (index: number, count: number, radius: string) => ({ transform: `rotate(${(360 / Math.max(count, 1)) * index}deg) translateY(-${radius})`,})const uprightStyle = (index: number, count: number) => ({ transform: `rotate(${-(360 / Math.max(count, 1)) * index}deg)`,})export const IntegrationOrbitBlock: React.FC<Props> = ({ className, disableInnerContainer, featuredLogo, heading, id, integrations, subtext,}) => { const items = integrations ?? [] const outer = items.slice(0, Math.ceil(items.length / 2)) const inner = items.slice(Math.ceil(items.length / 2)) return ( <section className={cn('container', className)} id={id ? `block-${id}` : undefined}> <div className="overflow-hidden rounded-frame border border-border/70 bg-card/35 px-6 py-14 sm:px-8 lg:px-12 lg:py-20"> <div className={cn('flex flex-col items-center gap-12', { 'mx-auto max-w-3xl': !disableInnerContainer, })} > <div className="group relative mx-auto flex aspect-square w-full max-w-xs items-center justify-center"> <div className="integration-orbit-ring absolute inset-0"> <div className="absolute inset-0 rounded-full border border-dashed border-border/70" /> {outer.map((item, index) => ( <div className="absolute left-1/2 top-1/2" key={item.id ?? `outer-${item.name}-${index}`} style={slotStyle(index, outer.length, '7.5rem')} > <div style={uprightStyle(index, outer.length)}> <div className="integration-orbit-logo"> <div className="flex size-12 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-xl border border-border/70 bg-background shadow-sm"> <Media resource={item.logo} imgClassName="size-6 w-auto object-contain" /> </div> </div> </div> </div> ))} </div> <div className="integration-orbit-ring-reverse absolute inset-[24%]"> <div className="absolute inset-0 rounded-full border border-dashed border-border/70" /> {inner.map((item, index) => ( <div className="absolute left-1/2 top-1/2" key={item.id ?? `inner-${item.name}-${index}`} style={slotStyle(index, inner.length, '4.25rem')} > <div style={uprightStyle(index, inner.length)}> <div className="integration-orbit-logo"> <div className="flex size-10 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-xl border border-border/70 bg-background shadow-sm"> <Media resource={item.logo} imgClassName="size-5 w-auto object-contain" /> </div> </div> </div> </div> ))} </div> {featuredLogo ? ( <div className="relative z-10 flex size-16 items-center justify-center rounded-2xl border border-foreground/25 bg-card shadow-lg"> <Media resource={featuredLogo} imgClassName="size-9 w-auto object-contain" /> </div> ) : null} </div> <div className="flex max-w-lg flex-col items-center gap-5 text-center"> <h2 className="text-balance text-2xl font-semibold tracking-heading text-foreground sm:text-3xl"> {heading} </h2> {subtext ? ( <p className="text-pretty text-sm text-muted-foreground sm:text-base">{subtext}</p> ) : null} </div> </div> </div> </section> )}import type { Field } from 'payload'/** * Shared field core for the Integration block family. * * Every integration variant (integration-grid, integration-cluster, * integration-split, integration-connect, integration-orbit, integration-list, * integration-marquee, integration-testimonial, …) spreads these fields first * and then appends its own variant-specific shape (for example the cluster * variants add a featured center mark, and the testimonial variant adds a * quote). Editing the shared heading/integrations shape here updates every * installed integration block at once, so the family never drifts * field-by-field across a repo. * * Each integration is an editable Media upload plus an accessible name, an * optional supporting description, and an optional link, so editors manage the * wall of partner/tool logos from the admin instead of shipping hardcoded * brand SVGs. Logo-only variants simply ignore the per-item description/href. * * Installed once per repo at `src/blocks/shared/integrationFields.ts`; re-running * `payload-components add integration-*` never overwrites a copy you have already edited. */export const integrationFields: Field[] = [ { name: 'heading', type: 'text', required: true, }, { name: 'subtext', type: 'textarea', }, { name: 'integrations', type: 'array', required: true, minRows: 2, maxRows: 12, admin: { initCollapsed: true, }, fields: [ { name: 'logo', type: 'upload', relationTo: 'media', required: true, }, { name: 'name', type: 'text', required: true, }, { name: 'description', type: 'textarea', }, { name: 'href', type: 'text', }, ], },]/** * Optional center brand mark, spread by the variants that arrange the * integration logos around a focal point (cluster, split, connect, orbit, * marquee). Left empty, the variant renders without a center mark. */export const integrationFeaturedMark: Field = { name: 'featuredLogo', type: 'upload', relationTo: 'media', admin: { description: 'Optional center brand mark shown at the focal point of the integration layout.', },}Installation
npx payload-components add integration-orbitCopy the files straight from the registry, then wire the Payload fragments by hand:
pnpm dlx shadcn@latest add https://www.payload-components.xyz/r/integration-orbit.jsonWhat it installs
Copies 3 source files into your project:
src/blocks/shared/integrationFields.tssharedsrc/blocks/IntegrationOrbit/config.tssrc/blocks/IntegrationOrbit/Component.tsx
…and makes 4 edits to wire the block into your project:
| Registers the block | src/collections/Pages/index.ts |
| Maps the renderer | src/blocks/RenderBlocks.tsx |
| Regenerates types | src/payload-types.ts |
| Regenerates the admin import map | src/app/(payload)/admin/importMap.js |
integrationFields.ts is the shared field core for this family — every variant composes it. Editing it updates each installed block at once, and re-running an install never overwrites a copy you have changed.Re-running the install converges: it detects existing wiring, skips it, and records install state in .payload-components/state.json.
Content model
heading, subtext, and integrations come from the shared integrationFields base. This variant
adds an optional featuredLogo at the center of the rings. The rings spin only on hover and respect
prefers-reduced-motion.
Prop
Type
Each item in integrations carries:
Prop
Type
Usage
IntegrationOrbit block to its layout.RenderBlocks on the frontend, fully typed — no extra wiring.Requirements
- Target
- payload-website-starter
- Payload
- v3
- Next.js
- 15 / 16
- shadcn UI
- none
Your project must already expose components.json, src/payload.config.ts, src/blocks/RenderBlocks.tsx, src/collections/Pages/index.ts — the surfaces payload-components add patches. The CLI verifies this against the support matrix before touching anything.
In this family
integration-gridintegration-clusterintegration-splitintegration-connectintegration-orbitcurrentintegration-listintegration-marqueeintegration-testimonial