Morphing Surface
Morphing Surface is a React and Tailwind CSS text component from SmoothUI, licensed MIT. Copy it and paste it into your project.
SmoothUI
MIT
text
What it is
Morphing Surface is a React and Tailwind CSS text component from SmoothUI, licensed MIT. Copy it and paste it into your project.
How to use it
Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.
Five free copies a month. No card.
Needs
npm i class-variance-authority motion
Licence
This component comes from SmoothUI and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.
Morphing Surface
"use client";
import SmoothButton from "@/components/smoothui/smooth-button";
import { cx } from "class-variance-authority";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import React from "react";
import SiriOrb from "../siri-orb";
import { useClickOutside } from "./use-click-outside";
const SPEED = 1;
const SUCCESS_DURATION = 1500;
const DOCK_HEIGHT = 44;
const FEEDBACK_BORDER_RADIUS = 14;
const DOCK_BORDER_RADIUS = 20;
const SPRING_STIFFNESS = 550;
const SPRING_DAMPING = 45;
const SPRING_MASS = 0.7;
const CLOSE_DELAY = 0.08;
interface FooterContext {
closeFeedback: () => void;
openFeedback: () => void;
showFeedback: boolean;
success: boolean;
}
const FooterContext = React.createContext({} as FooterContext);
const useFooter = () => React.useContext(FooterContext);
export function MorphSurface() {
const rootRef = React.useRef<HTMLDivElement>(null);
const feedbackRef = React.useRef<HTMLTextAreaElement | null>(null);
const [showFeedback, setShowFeedback] = React.useState(false);
const [success, setSuccess] = React.useState(false);
const shouldReduceMotion = useReducedMotion();
const closeFeedback = React.useCallback(() => {
setShowFeedback(false);
feedbackRef.current?.blur();
}, []);
const openFeedback = React.useCallback(() => {
setShowFeedback(true);
setTimeout(() => {
feedbackRef.current?.focus();
});
}, []);
const onFeedbackSuccess = React.useCallback(() => {
closeFeedback();
setSuccess(true);
setTimeout(() => {
setSuccess(false);
}, SUCCESS_DURATION);
}, [closeFeedback]);
useClickOutside(rootRef, closeFeedback);
const context = React.useMemo(
() => ({
closeFeedback,
openFeedback,
showFeedback,
success,
}),
[showFeedback, success, openFeedback, closeFeedback]
);
return (
<div
className="flex items-center justify-center"
style={{
height: FEEDBACK_HEIGHT,
width: FEEDBACK_WIDTH,
}}
>
<motion.div
animate={
shouldReduceMotion
? {}
: {
borderRadius: showFeedback
? FEEDBACK_BORDER_RADIUS
: DOCK_BORDER_RADIUS,
height: showFeedback ? FEEDBACK_HEIGHT : DOCK_HEIGHT,
width: showFeedback ? FEEDBACK_WIDTH : "auto",
}
}
className={cx(
"relative bottom-8 z-3 flex flex-col items-center overflow-hidden border bg-background max-sm:bottom-5"
)}
data-footer
initial={false}
ref={rootRef}
transition={
shouldReduceMotion
? { duration: 0 }
: {
damping: SPRING_DAMPING,
delay: showFeedback ? 0 : CLOSE_DELAY,
duration: 0.25,
mass: SPRING_MASS,
stiffness: SPRING_STIFFNESS / SPEED,
type: "spring" as const,
}
}
>
<FooterContext.Provider value={context}>
<Dock />
<Feedback onSuccess={onFeedbackSuccess} ref={feedbackRef} />
</FooterContext.Provider>
</motion.div>
</div>
);
}
function Dock() {
const { showFeedback, openFeedback } = useFooter();
const shouldReduceMotion = useReducedMotion();
return (
<footer className="mt-auto flex h-[44px] select-none items-center justify-center whitespace-nowrap">
<div className="flex items-center justify-center gap-2 px-3 max-sm:h-10 max-sm:px-2">
<div className="flex w-fit items-center gap-2">
<AnimatePresence mode="wait">
{showFeedback ? (
<motion.div
animate={shouldReduceMotion ? {} : { opacity: 0 }}
className="h-5 w-5"
exit={shouldReduceMotion ? {} : { opacity: 0 }}
initial={shouldReduceMotion ? {} : { opacity: 0 }}
key="placeholder"
transition={
shouldReduceMotion ? { duration: 0 } : { duration: 0.2 }
}
/>
) : (
<motion.div
animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1 }}
exit={
shouldReduceMotion
? { opacity: 0, transition: { duration: 0 } }
: { opacity: 0 }
}
initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0 }}
key="siri-orb"
transition={
shouldReduceMotion ? { duration: 0 } : { duration: 0.2 }
}
>
<SiriOrb
colors={{
bg: "oklch(22.64% 0 0)",
}}
size="24px"
/>
</motion.div>
)}
</AnimatePresence>
</div>
<SmoothButton
className="flex h-fit flex-1 justify-end rounded-full px-2 py-0.5!"
onClick={openFeedback}
type="button"
variant="ghost"
>
<span className="truncate">Ask AI</span>
</SmoothButton>
</div>
</footer>
);
}
const FEEDBACK_WIDTH = 360;
const FEEDBACK_HEIGHT = 200;
function Feedback({
ref,
onSuccess,
}: {
ref: React.Ref<HTMLTextAreaElement>;
onSuccess: () => void;
}) {
const { closeFeedback, showFeedback } = useFooter();
const shouldReduceMotion = useReducedMotion();
const submitRef = React.useRef<HTMLButtonElement>(null);
function onSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
onSuccess();
}
function onKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
if (e.key === "Escape") {
closeFeedback();
}
if (e.key === "Enter" && e.metaKey) {
e.preventDefault();
submitRef.current?.click();
}
}
return (
<form
className="absolute bottom-0"
onSubmit={onSubmit}
style={{
height: FEEDBACK_HEIGHT,
pointerEvents: showFeedback ? "all" : "none",
width: FEEDBACK_WIDTH,
}}
>
<AnimatePresence>
{showFeedback ? (
<motion.div
animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1 }}
className="flex h-full flex-col p-1"
exit={
shouldReduceMotion
? { opacity: 0, transition: { duration: 0 } }
: { opacity: 0 }
}
initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0 }}
transition={
shouldReduceMotion
? { duration: 0 }
: {
damping: SPRING_DAMPING,
duration: 0.25,
mass: SPRING_MASS,
stiffness: SPRING_STIFFNESS / SPEED,
type: "spring" as const,
}
}
>
<div className="flex justify-between py-1">
<p className="z-2 ml-[38px] flex select-none items-center gap-[6px] text-foreground">
AI Input
</p>
<button
className="right-4 mt-1 flex -translate-y-[3px] cursor-pointer select-none items-center justify-center gap-1 rounded-[12px] bg-transparent pr-1 text-center text-foreground"
ref={submitRef}
type="submit"
>
<Kbd>⌘</Kbd>
<Kbd className="w-fit">Enter</Kbd>
</button>
</div>
<textarea
className="h-full w-full resize-none scroll-py-2 rounded-md bg-primary p-4 outline-0"
name="message"
onKeyDown={onKeyDown}
placeholder="Ask me anything..."
ref={ref}
required
spellCheck={false}
/>
</motion.div>
) : null}
</AnimatePresence>
<AnimatePresence>
{showFeedback ? (
<motion.div
animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1 }}
className="absolute top-2 left-3"
exit={
shouldReduceMotion
? { opacity: 0, transition: { duration: 0 } }
: { opacity: 0 }
}
initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0 }}
transition={
shouldReduceMotion ? { duration: 0 } : { duration: 0.2 }
}
>
<SiriOrb
colors={{
bg: "oklch(22.64% 0 0)",
}}
size="24px"
/>
</motion.div>
) : null}
</AnimatePresence>
</form>
);
}
function Kbd({
children,
className,
}: {
children: string;
className?: string;
}) {
return (
<kbd
className={cx(
"flex h-6 w-fit items-center justify-center rounded-sm border bg-primary px-[6px] font-sans text-foreground",
className
)}
>
{children}
</kbd>
);
}
// Add default export for lazy loading
export default MorphSurface;
"use client";
import { type RefObject, useEffect } from "react";
type EventType =
| "mousedown"
| "mouseup"
| "touchstart"
| "touchend"
| "focusin"
| "focusout";
export function useClickOutside<T extends HTMLElement = HTMLElement>(
ref: RefObject<T | null> | RefObject<T | null>[],
handler: (event: Event) => void,
eventType: EventType = "mousedown"
): void {
useEffect(() => {
function callback(event: Event) {
const target = event.target as Node;
// Do nothing if the target is not connected element with document
if (!target?.isConnected) {
return;
}
const isOutside = Array.isArray(ref)
? ref
.filter((r) => Boolean(r.current))
.every((r) => r.current && !r.current.contains(target))
: ref.current && !ref.current.contains(target);
if (isOutside) {
handler(event);
}
}
window.addEventListener(eventType, callback);
return () => {
window.removeEventListener(eventType, callback);
};
}, [eventType, handler, ref]);
}