Data Reveal

PreviousNext

A animated data reveal component

Docs
rigiduicomponent

Preview

Loading preview…
r/new-york/data-reveal/data-reveal.tsx
"use client"
import { cn } from "@/lib/utils"
import { motion, AnimatePresence } from "motion/react"
import { useState, useEffect, ReactNode, createContext, useContext } from "react"

const defaultAnimation = {
    duration: 0.7,
    staggerDelay: 0.08,
    ease: [0.16, 1, 0.3, 1],
    containerScale: 0.8,
    itemScale: 0.7,
    blur: 4
}

interface DataRevealContextType {
    isRevealed: boolean
    toggle: () => void
    disabled: boolean
}

const DataRevealContext = createContext<DataRevealContextType | null>(null)

const useDataReveal = () => {
    const context = useContext(DataRevealContext)
    if (!context) {
        throw new Error('DataReveal compound components must be used within DataReveal')
    }
    return context
}

interface DataRevealProps {
    children: ReactNode
    defaultRevealed?: boolean
    onToggle?: (isRevealed: boolean) => void
    disabled?: boolean
    autoHide?: boolean
    autoHideDelay?: number
}

function DataReveal({
    children,
    defaultRevealed = false,
    onToggle,
    disabled = false,
    autoHide = false,
    autoHideDelay = 3000
}: DataRevealProps) {
    const [isRevealed, setIsRevealed] = useState(defaultRevealed)

    useEffect(() => {
        if (autoHide && isRevealed) {
            const timer = setTimeout(() => {
                handleToggle()
            }, autoHideDelay)
            return () => clearTimeout(timer)
        }
    }, [autoHide, isRevealed, autoHideDelay])

    const handleToggle = () => {
        if (disabled) return
        
        const newRevealedState = !isRevealed
        setIsRevealed(newRevealedState)
        onToggle?.(newRevealedState)
    }

    const contextValue: DataRevealContextType = {
        isRevealed,
        toggle: handleToggle,
        disabled
    }

    return (
        <DataRevealContext.Provider value={contextValue}>
            <div className="flex items-center">
                {children}
            </div>
        </DataRevealContext.Provider>
    )
}

interface RevealButtonProps {
    children?: ReactNode
    text?: string
    className?: string
}

function RevealButton({ children, text, className }: RevealButtonProps) {
    const { toggle, disabled } = useDataReveal()

    return (
        <div 
            className={cn(
                "flex items-center justify-center py-2 px-4 rounded-full border border-gray-300 bg-black cursor-pointer hover:border-gray-400 transition-colors",
                disabled && "opacity-50 cursor-not-allowed",
                className
            )}
            onClick={toggle}
        >
            <span>
                {children || text || 'Reveal'}
            </span>
        </div>
    )
}

interface RevealContainerProps {
    children: ReactNode
    className?: string
}

function RevealContainer({ children, className }: RevealContainerProps) {
    const { isRevealed } = useDataReveal()

    return (
        <AnimatePresence>
            {isRevealed && (
                <motion.div 
                    className={cn("flex gap-2 ml-2", className)}
                    initial={{ 
                        opacity: 0, 
                        width: 0,
                        scale: defaultAnimation.containerScale
                    }}
                    animate={{ 
                        opacity: 1, 
                        width: "auto",
                        scale: 1
                    }}
                    exit={{ 
                        opacity: 0, 
                        width: 0,
                        scale: defaultAnimation.containerScale
                    }}
                    transition={{ 
                        duration: defaultAnimation.duration * 0.7,
                        ease: defaultAnimation.ease as any
                    }}
                >
                    {children}
                </motion.div>
            )}
        </AnimatePresence>
    )
}

interface RevealItemProps {
    children?: ReactNode
    content?: string
    index?: number
    className?: string
}

function RevealItem({ children, content, index = 0, className }: RevealItemProps) {
    return (
        <motion.div
            className={cn(
                "flex items-center justify-center py-2 px-4 rounded-full border border-gray-300 whitespace-nowrap",
                className
            )}
            initial={{ 
                opacity: 0,
                x: -60,
                scale: defaultAnimation.itemScale,
                filter: `blur(${defaultAnimation.blur}px)`
            }}
            animate={{ 
                opacity: 1,
                x: 0,
                scale: 1,
                filter: "blur(0px)"
            }}
            exit={{ 
                opacity: 0,
                x: -40,
                scale: defaultAnimation.itemScale * 1.1,
                filter: `blur(${defaultAnimation.blur / 2}px)`
            }}
            transition={{ 
                duration: defaultAnimation.duration,
                delay: index * defaultAnimation.staggerDelay,
                ease: defaultAnimation.ease as any,
                filter: { duration: defaultAnimation.duration * 0.6 }
            }}
        >
            <span>
                {children || content}
            </span>
        </motion.div>
    )
}

DataReveal.Button = RevealButton
DataReveal.Container = RevealContainer
DataReveal.Item = RevealItem

export { DataReveal, RevealButton, RevealContainer, RevealItem }
export type { DataRevealProps, RevealButtonProps, RevealContainerProps, RevealItemProps }

Installation

npx shadcn@latest add @rigidui/data-reveal

Usage

import { DataReveal } from "@/components/data-reveal"
<DataReveal />