nusaiba
itemfoundationsource: ok · hit
Nusaiba Foundation
Motion tokens, design-system UI primitives, brand logos, infinite slider, and button-scale theme. Installed automatically with every block.
npx shadcn add "https://nusaiba.dev/r/base-nova/foundation.json"free
Where it goes
logo-cloud
Interaction
carousel
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · mediumanimationmotion
Registry dependencies
buttoninput
Files9 · source cached 10/14/2026, 1:15:44 PM
registry/foundation/motion-tokens.ts
/**
* Canonical motion tokens for Nusaiba blocks.
* Installed via @nusaiba/foundation — keep in sync with site motion docs.
*/
export const motionSpring = {
default: { type: "spring" as const, stiffness: 380, damping: 28 },
soft: { type: "spring" as const, stiffness: 300, damping: 32 },
snappy: { type: "spring" as const, stiffness: 420, damping: 22 },
visual: { type: "spring" as const, stiffness: 380, damping: 24 },
} as const;
export const motionDuration = {
fast: 0.2,
base: 0.5,
slow: 0.8,
} as const;
export const motionStagger = {
item: 0.06,
group: 0.08,
} as const;
export const motionViewport = {
once: true,
margin: "-10%",
} as const;
export const motionScale = {
hover: 1.02,
press: 0.98,
} as const;
/** Default spring — use in marketing sections and landing UI. */
export const defaultSpring = motionSpring.default;
registry/foundation/section-scroll.tsx
"use client";
import {
createContext,
useContext,
type ReactNode,
type RefObject,
} from "react";
import {
useScroll,
useSpring,
useTransform,
type UseScrollOptions,
} from "motion/react";
const SectionScrollContainerContext =
createContext<RefObject<HTMLElement | null> | null>(null);
/** Sticky stack pin offsets — shared by features/blog stack blocks. */
export const STACK_CARD_TOP_BASE = 88;
export const STACK_CARD_TOP_STEP = 28;
/** Spring follow for scroll-linked card motion — soft settle, no bounce. */
export const stackCardScrollSpring = {
stiffness: 120,
damping: 34,
mass: 0.28,
restDelta: 0.0008,
} as const;
export function SectionScrollContainerProvider({
containerRef,
children,
}: {
containerRef: RefObject<HTMLElement | null>;
children: ReactNode;
}) {
return (
<SectionScrollContainerContext.Provider value={containerRef}>
{children}
</SectionScrollContainerContext.Provider>
);
}
/** useScroll with optional preview scrollport (Nusaiba block preview). Falls back to document scroll. */
export function useSectionScroll(options: UseScrollOptions = {}) {
const containerRef = useContext(SectionScrollContainerContext);
return useScroll({
...options,
...(containerRef ? { container: containerRef } : {}),
});
}
/** Smooth sticky-stack transforms (y, scale, blur) for scroll-linked cards. */
export function useStackCardScrollMotion(
target: RefObject<HTMLElement | null>,
reduceMotion: boolean,
) {
const { scrollYProgress } = useSectionScroll({
target,
offset: ["start 0.97", "start 0.36"],
});
const smoothProgress = useSpring(scrollYProgress, stackCardScrollSpring);
const progress = reduceMotion ? scrollYProgress : smoothProgress;
const y = useTransform(progress, [0, 0.55, 1], [52, 10, 0]);
const scale = useTransform(progress, [0, 0.45, 1], [0.982, 0.992, 1]);
const filter = useTransform(progress, [0, 0.7, 1], ["blur(3px)", "blur(1px)", "blur(0px)"]);
return { y, scale, filter };
}
registry/foundation/button.tsx
import { Button as ButtonPrimitive } from "@base-ui/react/button"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonPadding =
"py-[var(--button-py)] px-[var(--button-px)] has-[>svg]:px-[var(--button-px-icon)]"
const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center gap-[var(--button-gap)] whitespace-nowrap rounded-[var(--button-radius)] text-sm font-medium transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 active:scale-[0.98] disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
outline:
"border border-border bg-background shadow-none hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: buttonPadding,
xs: "gap-1 py-1.5 px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "py-[var(--button-py-sm)] px-[var(--button-px-sm)] text-xs has-[>svg]:px-[var(--button-px-icon-sm)] [&_svg:not([class*='size-'])]:size-3.5",
lg: "py-[var(--button-py-lg)] px-[var(--button-px-lg)] has-[>svg]:px-[var(--button-px-icon-lg)]",
icon: "size-[var(--button-size)]",
"icon-xs": "size-6 [&_svg:not([class*='size-'])]:size-3",
"icon-sm": "size-[var(--button-size-sm)]",
"icon-lg": "size-[var(--button-size-lg)]",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant = "default",
size = "default",
...props
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
return (
<ButtonPrimitive
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }
registry/foundation/input.tsx
import * as React from "react"
import { Input as InputPrimitive } from "@base-ui/react/input"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<InputPrimitive
type={type}
data-slot="input"
className={cn(
"flex h-[var(--button-size)] w-full min-w-0 rounded-[var(--button-radius)] border border-input bg-transparent px-[var(--button-px)] py-[var(--button-py)] text-sm transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className
)}
{...props}
/>
)
}
export { Input }
registry/foundation/nusaiba-font.tsx
import { JetBrains_Mono, Plus_Jakarta_Sans } from "next/font/google";
export const nusaibaSans = Plus_Jakarta_Sans({
variable: "--font-sans",
subsets: ["latin"],
});
export const nusaibaMono = JetBrains_Mono({
variable: "--font-mono",
subsets: ["latin"],
});
/** Apply on `<html>` — matches nusaiba.dev typography. */
export function nusaibaFontClassName() {
return `${nusaibaSans.variable} ${nusaibaMono.variable}`;
}
registry/foundation/layout-contract.tsx
import type { ReactNode } from "react";
import { cn } from "@/lib/utils";
type BlockSpacingMode = "section" | "compact" | "none";
type BlockDividerMode = "none" | "top" | "bottom" | "both";
const shellSpacingClass: Record<BlockSpacingMode, string> = {
section: "px-4 py-12 md:px-6 md:py-16",
compact: "px-4 py-8 md:px-6 md:py-12",
none: "",
};
const sectionDividerClass: Record<BlockDividerMode, string> = {
none: "",
top: "border-border border-t",
bottom: "border-border border-b",
both: "border-border border-y",
};
export function getSectionShellClass({
spacingMode = "section",
dividerMode = "none",
className,
}: {
spacingMode?: BlockSpacingMode;
dividerMode?: BlockDividerMode;
className?: string;
}) {
return cn(shellSpacingClass[spacingMode], sectionDividerClass[dividerMode], className);
}
export function SectionShell({
children,
className,
spacingMode = "section",
dividerMode = "none",
...props
}: {
children: ReactNode;
className?: string;
spacingMode?: BlockSpacingMode;
dividerMode?: BlockDividerMode;
} & React.ComponentProps<"section">) {
return (
<section className={getSectionShellClass({ spacingMode, dividerMode, className })} {...props}>
{children}
</section>
);
}
export function ContentRail({
children,
className,
maxWidth = "max-w-6xl",
...props
}: {
children: ReactNode;
className?: string;
maxWidth?: string;
} & React.ComponentProps<"div">) {
return (
<div className={cn("mx-auto w-full", maxWidth, className)} {...props}>
{children}
</div>
);
}
export function BleedLayer({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
return <div className={cn("w-full", className)}>{children}</div>;
}
registry/foundation/infinite-slider.tsx
"use client";
import { cn } from "@/lib/utils";
import { animate, motion, useMotionValue } from "motion/react";
import { useEffect, useState } from "react";
import useMeasure from "react-use-measure";
export type InfiniteSliderProps = {
children: React.ReactNode;
gap?: number;
speed?: number;
speedOnHover?: number;
direction?: "horizontal" | "vertical";
reverse?: boolean;
className?: string;
};
export function InfiniteSlider({
children,
gap = 16,
speed = 100,
speedOnHover,
direction = "horizontal",
reverse = false,
className,
}: InfiniteSliderProps) {
const [currentSpeed, setCurrentSpeed] = useState(speed);
const [ref, { width, height }] = useMeasure();
const translation = useMotionValue(0);
const [isTransitioning, setIsTransitioning] = useState(false);
useEffect(() => {
let controls: ReturnType<typeof animate> | undefined;
const size = direction === "horizontal" ? width : height;
const contentSize = size + gap;
const from = reverse ? -contentSize / 2 : 0;
const to = reverse ? 0 : -contentSize / 2;
const distanceToTravel = Math.abs(to - from);
const duration = distanceToTravel / currentSpeed;
if (isTransitioning) {
const remainingDistance = Math.abs(translation.get() - to);
const transitionDuration = remainingDistance / currentSpeed;
controls = animate(translation, [translation.get(), to], {
ease: "linear",
duration: transitionDuration,
onComplete: () => setIsTransitioning(false),
});
} else {
controls = animate(translation, [from, to], {
ease: "linear",
duration,
repeat: Number.POSITIVE_INFINITY,
repeatType: "loop",
repeatDelay: 0,
onRepeat: () => translation.set(from),
});
}
return controls?.stop;
}, [
translation,
currentSpeed,
width,
height,
gap,
isTransitioning,
direction,
reverse,
]);
const hoverProps = speedOnHover
? {
onHoverStart: () => {
setIsTransitioning(true);
setCurrentSpeed(speedOnHover);
},
onHoverEnd: () => {
setIsTransitioning(true);
setCurrentSpeed(speed);
},
}
: {};
return (
<div className={cn("overflow-hidden", className)}>
<motion.div
className={cn(
"flex w-max",
direction === "horizontal" ? "flex-row" : "flex-col",
)}
ref={ref}
style={{
...(direction === "horizontal"
? { x: translation }
: { y: translation }),
gap: `${gap}px`,
}}
{...hoverProps}
>
{children}
{children}
</motion.div>
</div>
);
}
public/images/logo-nusaiba-wordmark.png
�PNG
IHDR � � |� � pHYs ,K ,K�=�� sRGB ��� gAMA ���a tEXtSoftware Figma���c �IDATx��ݏ\U����""�i0�,p5��UM@�t��%lz _�"X,��H[:Rmi5.�ܗ�0�`־�+\7}�j���+�Y�TT�w�z��P ��8#r?O�v8�/sΘc��G���t$��e��|�� �҆-��rbmm퇻��G���?���Go�j�n��{[����`0�n����� @EZP��@�������O��H�'���Ƕ��-7 @I� ������_���>nˆ*�j�<l�}yM�M K"`Tgc}}���p��eU*YD���hgg�_��? �0�WKN�Q�dV�V��`0�'��3 0&@�Nu:�g�,*��+Z� fD�(��N������IE��ﻷn�zO �� ��#G�<9�6�j�4�V����D5 �:�������`0�������n�������� �L��xK��gL-q��j��/kkk� c� ��p������J��e� &!`��D�\r��!{|��� ��� 0;�\��H5\R��w�� `TK f�d[����}�֭[� �f���SZ�ز- lM ��[��
����iˇ �!l̾gc�o(!��o�- �`L�C��-\Rh��c��ٗ���<`����N��#�̓a}3,j�Z7vvv��/����A ��0�����`0xB�t:��s�
���y�䑵��'vwwO�r����p8�wn�������[��u[����|,'