Terminal
Terminal is a React and Tailwind CSS data component from Magic UI, licensed MIT. Copy it and paste it into your project.
Magic UI
MIT
data
What it is
Terminal is a React and Tailwind CSS data component from Magic UI, 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.
No packages beyond the project baseline
Licence
This component comes from Magic UI and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.
Terminal
"use client"
import {
Children,
createContext,
useContext,
useEffect,
useMemo,
useRef,
useState,
type ComponentType,
type RefAttributes,
} from "react"
import {
motion,
useInView,
type DOMMotionComponents,
type HTMLMotionProps,
type MotionProps,
} from "motion/react"
import { cn } from "@/lib/utils"
interface SequenceContextValue {
completeItem: (index: number) => void
activeIndex: number
sequenceStarted: boolean
}
const SequenceContext = createContext<SequenceContextValue | null>(null)
const useSequence = () => useContext(SequenceContext)
const ItemIndexContext = createContext<number | null>(null)
const useItemIndex = () => useContext(ItemIndexContext)
const motionElements = {
article: motion.article,
div: motion.div,
h1: motion.h1,
h2: motion.h2,
h3: motion.h3,
h4: motion.h4,
h5: motion.h5,
h6: motion.h6,
li: motion.li,
p: motion.p,
section: motion.section,
span: motion.span,
} as const
type MotionElementType = Extract<
keyof DOMMotionComponents,
keyof typeof motionElements
>
type TerminalTypingMotionComponent = ComponentType<
Omit<HTMLMotionProps<"span">, "ref"> & RefAttributes<HTMLElement>
>
interface AnimatedSpanProps extends MotionProps {
children: React.ReactNode
delay?: number
className?: string
startOnView?: boolean
}
export const AnimatedSpan = ({
children,
delay = 0,
className,
startOnView = false,
...props
}: AnimatedSpanProps) => {
const elementRef = useRef<HTMLDivElement | null>(null)
const isInView = useInView(elementRef as React.RefObject<Element>, {
amount: 0.3,
once: true,
})
const sequence = useSequence()
const itemIndex = useItemIndex()
const [hasStarted, setHasStarted] = useState(false)
useEffect(() => {
if (!sequence || itemIndex === null) return
if (!sequence.sequenceStarted) return
if (hasStarted) return
if (sequence.activeIndex === itemIndex) {
setHasStarted(true)
}
}, [sequence, hasStarted, itemIndex])
const shouldAnimate = sequence ? hasStarted : startOnView ? isInView : true
return (
<motion.div
ref={elementRef}
initial={{ opacity: 0, y: -5 }}
animate={shouldAnimate ? { opacity: 1, y: 0 } : { opacity: 0, y: -5 }}
transition={{ duration: 0.3, delay: sequence ? 0 : delay / 1000 }}
className={cn("grid text-sm font-normal tracking-tight", className)}
onAnimationComplete={() => {
if (!sequence) return
if (itemIndex === null) return
sequence.completeItem(itemIndex)
}}
{...props}
>
{children}
</motion.div>
)
}
interface TypingAnimationProps extends Omit<MotionProps, "children"> {
children: string
className?: string
duration?: number
delay?: number
as?: MotionElementType
startOnView?: boolean
}
export const TypingAnimation = ({
children,
className,
duration = 60,
delay = 0,
as: Component = "span",
startOnView = true,
...props
}: TypingAnimationProps) => {
if (typeof children !== "string") {
throw new Error("TypingAnimation: children must be a string. Received:")
}
const MotionComponent = motionElements[
Component
] as TerminalTypingMotionComponent
const [displayedText, setDisplayedText] = useState<string>("")
const [started, setStarted] = useState(false)
const elementRef = useRef<HTMLElement | null>(null)
const isInView = useInView(elementRef as React.RefObject<Element>, {
amount: 0.3,
once: true,
})
const sequence = useSequence()
const itemIndex = useItemIndex()
const hasSequence = sequence !== null
const sequenceStarted = sequence?.sequenceStarted ?? false
const sequenceActiveIndex = sequence?.activeIndex ?? null
const sequenceCompleteItemRef = useRef<
SequenceContextValue["completeItem"] | null
>(null)
const sequenceItemIndexRef = useRef<number | null>(null)
useEffect(() => {
sequenceCompleteItemRef.current = sequence?.completeItem ?? null
sequenceItemIndexRef.current = itemIndex
}, [sequence?.completeItem, itemIndex])
useEffect(() => {
let startTimeout: ReturnType<typeof setTimeout> | null = null
if (hasSequence && itemIndex !== null) {
if (sequenceStarted && !started && sequenceActiveIndex === itemIndex) {
setStarted(true)
}
} else if (!startOnView || isInView) {
startTimeout = setTimeout(() => setStarted(true), delay)
}
return () => {
if (startTimeout !== null) {
clearTimeout(startTimeout)
}
}
}, [
delay,
startOnView,
isInView,
started,
hasSequence,
sequenceActiveIndex,
sequenceStarted,
itemIndex,
])
useEffect(() => {
let typingEffect: ReturnType<typeof setInterval> | null = null
if (started) {
let i = 0
typingEffect = setInterval(() => {
if (i < children.length) {
setDisplayedText(children.substring(0, i + 1))
i++
} else {
if (typingEffect !== null) {
clearInterval(typingEffect)
}
const completeItem = sequenceCompleteItemRef.current
const currentItemIndex = sequenceItemIndexRef.current
if (completeItem && currentItemIndex !== null) {
completeItem(currentItemIndex)
}
}
}, duration)
}
return () => {
if (typingEffect !== null) {
clearInterval(typingEffect)
}
}
}, [children, duration, started])
return (
<MotionComponent
ref={elementRef}
className={cn("text-sm font-normal tracking-tight", className)}
{...props}
>
{displayedText}
</MotionComponent>
)
}
interface TerminalProps {
children: React.ReactNode
className?: string
sequence?: boolean
startOnView?: boolean
}
export const Terminal = ({
children,
className,
sequence = true,
startOnView = true,
}: TerminalProps) => {
const containerRef = useRef<HTMLDivElement | null>(null)
const isInView = useInView(containerRef as React.RefObject<Element>, {
amount: 0.3,
once: true,
})
const [activeIndex, setActiveIndex] = useState(0)
const sequenceHasStarted = sequence ? !startOnView || isInView : false
const contextValue = useMemo<SequenceContextValue | null>(() => {
if (!sequence) return null
return {
completeItem: (index: number) => {
setActiveIndex((current) => (index === current ? current + 1 : current))
},
activeIndex,
sequenceStarted: sequenceHasStarted,
}
}, [sequence, activeIndex, sequenceHasStarted])
const wrappedChildren = useMemo(() => {
if (!sequence) return children
const array = Children.toArray(children)
return array.map((child, index) => (
<ItemIndexContext.Provider key={index} value={index}>
{child as React.ReactNode}
</ItemIndexContext.Provider>
))
}, [children, sequence])
const content = (
<div
ref={containerRef}
className={cn(
"border-border bg-background z-0 h-full max-h-100 w-full max-w-lg rounded-xl border",
className
)}
>
<div className="border-border flex flex-col gap-y-2 border-b p-4">
<div className="flex flex-row gap-x-2">
<div className="h-2 w-2 rounded-full bg-red-500"></div>
<div className="h-2 w-2 rounded-full bg-yellow-500"></div>
<div className="h-2 w-2 rounded-full bg-green-500"></div>
</div>
</div>
<pre className="p-4">
<code className="grid gap-y-1 overflow-auto">{wrappedChildren}</code>
</pre>
</div>
)
if (!sequence) return content
return (
<SequenceContext.Provider value={contextValue}>
{content}
</SequenceContext.Provider>
)
}