# Contact 1

> Split contact with channel list and full message form — scroll-reveal stagger.

A shadcn/ui block from the [@nusaiba](https://nusaiba.dev) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/nusaiba/contact-1).

- Page, with a live preview: https://shadcn.exchange/r/nusaiba/contact-1
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/nusaiba/contact-1-light-71e010e85dd00478.png
- Registry item: https://nusaiba.dev/r/base-nova/contact-1.json
- Categories: contact
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://nusaiba.dev/r/base-nova/contact-1.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `motion`
- Registry items: `@nusaiba/foundation`, `button`, `input`, `label`

## Files

### `registry/blocks/contact/contact-data.ts`

```ts
export const contactSite = {
	name: "Nusaiba",
	email: "hello@nusaiba.dev",
	domain: "nusaiba.dev",
	namespace: "@nusaiba",
} as const;

export const contactFormCopy = {
	submit: "Send message",
	sending: "Sending…",
	namePlaceholder: "Jane",
	lastNamePlaceholder: "Doe",
	emailPlaceholder: "you@nusaiba.dev",
	messagePlaceholder: "Tell us about your landing page or Pro licensing needs…",
} as const;

export const contactSplitCopy = {
	eyebrow: "Contact",
	title: "Questions about the registry?",
	description:
		"Ask about Pro access, commercial licensing, team seats, or custom block installs — we respond within one business day.",
} as const;

export const contactCompactCopy = {
	eyebrow: "Reach us",
	title: "Send a note",
	description:
		"Pro licensing, team plans, or help picking blocks for your landing page — we read every message.",
} as const;

export const contactVisualCopy = {
	eyebrow: "Support",
	title: "Let's talk about your page",
	description:
		"Share your stack and timeline — we'll point you to the right blocks and install path.",
} as const;

export const contactBandCopy = {
	title: "Need help before you buy?",
	description: "Leave your email — we'll send install tips and Pro licensing details.",
	submit: "Request info",
} as const;

export const contactTopics = [
	{ id: "pro", label: "Pro license" },
	{ id: "team", label: "Team plan" },
	{ id: "blocks", label: "Block advice" },
	{ id: "other", label: "Other" },
] as const;

export type ContactTopicId = (typeof contactTopics)[number]["id"];

export const contactSubjects = [
	"Pro commercial license",
	"Team workspace (10 seats)",
	"Custom block install",
	"Partnership / press",
] as const;

export const contactChannels = [
	{
		id: "email",
		label: "Email",
		value: contactSite.email,
		href: `mailto:${contactSite.email}`,
	},
	{
		id: "docs",
		label: "Docs",
		value: `${contactSite.domain}/docs`,
		href: "/docs",
	},
	{
		id: "registry",
		label: "Registry",
		value: contactSite.namespace,
		href: "/blocks",
	},
] as const;

export const contactEnterpriseCopy = {
	eyebrow: "Enterprise",
	title: "Talk to us about team access",
	description:
		"Agency or product team rolling out blocks across client landing pages — tell us seat count and timeline.",
} as const;

export const contactFramedCopy = {
	eyebrow: "Contact",
	title: "Get in touch",
	description: "We help builders install, customize, and license Nusaiba blocks for production sites.",
} as const;
```

### `registry/blocks/contact/contact-primitives.tsx`

```tsx
import Link from "next/link";
import type { LucideIcon } from "lucide-react";
import { BookOpenIcon, BoxesIcon, MailIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import type { ContactTopicId } from "./contact-data";
import { contactTopics } from "./contact-data";

const channelIcons: Record<string, LucideIcon> = {
	email: MailIcon,
	docs: BookOpenIcon,
	registry: BoxesIcon,
};

export function ContactMessageField({
	id,
	placeholder,
	rows = 5,
}: {
	id: string;
	placeholder: string;
	rows?: number;
}) {
	return (
		<textarea
			className={cn(
				"flex min-h-28 w-full resize-y rounded-[var(--button-radius)] border border-input bg-transparent px-[var(--button-px)] py-[var(--button-py)] text-sm outline-none",
				"placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50",
			)}
			id={id}
			placeholder={placeholder}
			rows={rows}
		/>
	);
}

export function ContactChannelList({
	channels,
}: {
	channels: readonly {
		id: string;
		label: string;
		value: string;
		href: string;
	}[];
}) {
	return (
		<ul className="space-y-4">
			{channels.map((channel) => {
				const Icon = channelIcons[channel.id] ?? MailIcon;

				return (
					<li className="flex items-start gap-3" key={channel.id}>
						<Icon className="mt-0.5 size-4 shrink-0 text-muted-foreground" />
						<div>
							<p className="font-medium text-sm">{channel.label}</p>
							<Link
								className="text-muted-foreground text-sm underline-offset-4 hover:text-foreground hover:underline"
								href={channel.href}
							>
								{channel.value}
							</Link>
						</div>
					</li>
				);
			})}
		</ul>
	);
}

export function ContactTopicPills({
	active,
	onChange,
}: {
	active: ContactTopicId;
	onChange: (topic: ContactTopicId) => void;
}) {
	return (
		<div className="flex flex-wrap gap-2">
			{contactTopics.map((topic) => (
				<button
					aria-pressed={active === topic.id}
					className={cn(
						"rounded-full border px-3 py-1.5 font-medium text-xs transition-[color,transform,border-color]",
						"active:scale-[0.96]",
						active === topic.id
							? "border-foreground bg-foreground text-background"
							: "border-border text-muted-foreground hover:text-foreground",
					)}
					key={topic.id}
					onClick={() => onChange(topic.id)}
					type="button"
				>
					{topic.label}
				</button>
			))}
		</div>
	);
}
```

### `registry/blocks/contact/1/contact-section.tsx`

```tsx
"use client";

import { useRef } from "react";
import { motion, useInView, useReducedMotion } from "motion/react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { motionStagger, motionViewport } from "@/lib/motion-tokens";
import { cn } from "@/lib/utils";
import {
	contactChannels,
	contactFormCopy,
	contactSplitCopy,
} from "../contact-data";
import { ContactChannelList, ContactMessageField } from "../contact-primitives";
import { SectionShell, ContentRail } from "@/components/layout-contract";

const spring = { type: "spring" as const, duration: 0.3, bounce: 0 };

export function ContactSection() {
	const reduceMotion = useReducedMotion();
	const ref = useRef<HTMLElement>(null);
	const inView = useInView(ref, motionViewport);

	const itemVariants = reduceMotion
		? { hidden: { opacity: 1 }, visible: { opacity: 1 } }
		: {
				hidden: { opacity: 0, filter: "blur(4px)", y: 8 },
				visible: { opacity: 1, filter: "blur(0px)", y: 0, transition: spring },
			};

	return (
		<SectionShell spacingMode="section" ref={ref}>
			<ContentRail maxWidth="max-w-5xl" className="grid gap-12 lg:grid-cols-2 lg:gap-16">
				<motion.div
					animate={inView ? "visible" : "hidden"}
					initial="hidden"
					variants={{
						hidden: {},
						visible: { transition: { staggerChildren: motionStagger.item, delayChildren: 0.04 } },
					}}
				>
					<motion.p
						className="font-medium text-primary text-xs uppercase tracking-[0.22em]"
						variants={itemVariants}
					>
						{contactSplitCopy.eyebrow}
					</motion.p>
					<motion.h2
						className="mt-3 text-balance font-medium text-2xl tracking-tight md:text-4xl"
						variants={itemVariants}
					>
						{contactSplitCopy.title}
					</motion.h2>
					<motion.p
						className="mt-4 text-pretty text-muted-foreground text-sm leading-relaxed md:text-base"
						variants={itemVariants}
					>
						{contactSplitCopy.description}
					</motion.p>
					<motion.div className="mt-8" variants={itemVariants}>
						<ContactChannelList channels={contactChannels} />
					</motion.div>
				</motion.div>

				<motion.form
					animate={inView ? "visible" : "hidden"}
					className="space-y-4"
					initial="hidden"
					variants={{
						hidden: {},
						visible: { transition: { staggerChildren: motionStagger.item, delayChildren: 0.08 } },
					}}
				>
					<motion.div className="grid gap-4 sm:grid-cols-2" variants={itemVariants}>
						<div className="space-y-2">
							<Label htmlFor="contact-1-first">First name</Label>
							<Input
								autoComplete="given-name"
								id="contact-1-first"
								placeholder={contactFormCopy.namePlaceholder}
							/>
						</div>
						<div className="space-y-2">
							<Label htmlFor="contact-1-last">Last name</Label>
							<Input
								autoComplete="family-name"
								id="contact-1-last"
								placeholder={contactFormCopy.lastNamePlaceholder}
							/>
						</div>
					</motion.div>
					<motion.div className="space-y-2" variants={itemVariants}>
						<Label htmlFor="contact-1-email">Work email</Label>
						<Input
							autoComplete="email"
							id="contact-1-email"
							placeholder={contactFormCopy.emailPlaceholder}
							type="email"
						/>
					</motion.div>
					<motion.div className="space-y-2" variants={itemVariants}>
						<Label htmlFor="contact-1-message">Message</Label>
						<ContactMessageField
							id="contact-1-message"
							placeholder={contactFormCopy.messagePlaceholder}
						/>
					</motion.div>
					<motion.div variants={itemVariants}>
						<Button
							className={cn("w-full active:scale-[0.96] transition-transform")}
							type="button"
						>
							{contactFormCopy.submit}
						</Button>
					</motion.div>
				</motion.form>
			</ContentRail>
		</SectionShell>
	);
}
```
