21 lines
7.0 KiB
JSON
21 lines
7.0 KiB
JSON
{
|
|
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
"name": "stars-scrolling-wheel",
|
|
"type": "registry:ui",
|
|
"title": "Stars Scrolling Wheel",
|
|
"description": "A scrolling wheel that displays stars count.",
|
|
"dependencies": [
|
|
"motion"
|
|
],
|
|
"registryDependencies": [
|
|
"https://animate-ui.com/r/star-icon"
|
|
],
|
|
"files": [
|
|
{
|
|
"path": "registry/components/stars-scrolling-wheel/index.tsx",
|
|
"content": "'use client';\n\nimport * as React from 'react';\nimport {\n AnimatePresence,\n motion,\n useMotionValue,\n useSpring,\n useTransform,\n useInView,\n type SpringOptions,\n type UseInViewOptions,\n} from 'motion/react';\n\nimport { cn } from '@/lib/utils';\nimport { Star } from '@/components/animate-ui/icons/star';\n\nconst formatter = new Intl.NumberFormat('en-US');\n\nfunction generateRange(\n max: number,\n step: number,\n sideItemsCount: number,\n): number[] {\n const result: number[] = [];\n const end = max + sideItemsCount * step;\n for (let value = end; value >= 0; value -= step) {\n result.push(value);\n }\n return result;\n}\n\ntype StarsScrollingWheelProps = {\n stars: number;\n step?: number;\n itemHeight?: number;\n sideItemsCount?: number;\n transition?: SpringOptions;\n inView?: boolean;\n inViewOnce?: boolean;\n inViewMargin?: UseInViewOptions['margin'];\n delay?: number;\n} & React.ComponentProps<'div'>;\n\nfunction StarsScrollingWheel({\n ref,\n stars,\n step = 100,\n itemHeight = 48,\n sideItemsCount = 2,\n transition = { stiffness: 90, damping: 30 },\n inView = false,\n inViewOnce = true,\n inViewMargin = '0px',\n delay = 0,\n className,\n style,\n ...props\n}: StarsScrollingWheelProps) {\n const containerRef = React.useRef<HTMLDivElement>(null);\n React.useImperativeHandle(ref, () => containerRef.current as HTMLDivElement);\n\n const inViewResult = useInView(containerRef, {\n once: inViewOnce,\n margin: inViewMargin,\n });\n const isInView = !inView || inViewResult;\n\n const displayedItemsCount = 1 + sideItemsCount * 2;\n const range = React.useMemo(\n () => generateRange(stars, step, sideItemsCount),\n [stars, step, sideItemsCount],\n );\n\n const initialY = -(itemHeight * sideItemsCount);\n const finalY = itemHeight * (range.length - displayedItemsCount);\n\n const yMotion = useMotionValue(initialY);\n const ySpring = useSpring(yMotion, transition);\n\n React.useEffect(() => {\n if (!isInView) return;\n const timer = setTimeout(() => {\n yMotion.set(finalY);\n }, delay);\n return () => clearTimeout(timer);\n }, [isInView, finalY, yMotion, delay]);\n\n const currentIndex = useTransform(\n ySpring,\n (y) => y / itemHeight + sideItemsCount,\n );\n const currentValue = useTransform(currentIndex, (idx) => idx * step);\n const completedTransform = useTransform(\n currentValue,\n (val) => val >= stars * 0.99,\n );\n\n const [isCompleted, setCompleted] = React.useState<boolean>(\n completedTransform.get(),\n );\n React.useEffect(() => {\n const unsubscribe = completedTransform.on('change', (latest) => {\n if (latest) setCompleted(true);\n });\n return unsubscribe;\n }, [completedTransform]);\n\n return (\n <div\n ref={containerRef}\n className={cn(\n 'relative overflow-hidden w-[200px] bg-background',\n className,\n )}\n style={{ height: itemHeight * displayedItemsCount, ...style }}\n {...props}\n >\n <div\n className=\"absolute z-2 top-0 inset-x-0 bg-gradient-to-t from-transparent to-background\"\n style={{ height: itemHeight }}\n />\n <div\n className=\"absolute z-1 top-0 inset-x-0 bg-background/60\"\n style={{ height: itemHeight * sideItemsCount }}\n />\n\n <div\n className=\"absolute z-1 bottom-0 inset-x-0 bg-gradient-to-b from-transparent to-background\"\n style={{ height: itemHeight }}\n />\n <div\n className=\"absolute z-1 bottom-0 inset-x-0 bg-background/60\"\n style={{ height: itemHeight * sideItemsCount }}\n />\n\n <div className=\"absolute inset-x-0 top-1/2 -translate-y-1/2 flex items-center justify-center\">\n <div\n className=\"w-full bg-muted rounded-xl flex items-center justify-start px-6\"\n style={{ height: itemHeight }}\n >\n <div className=\"relative inline-flex size-[28px] shrink-0\">\n <Star\n animation=\"fill\"\n animate={isCompleted}\n className=\"text-yellow-500\"\n />\n <AnimatePresence>\n {isCompleted && (\n <>\n <motion.div\n className=\"absolute inset-0 rounded-full\"\n style={{\n background:\n 'radial-gradient(circle, rgba(255,215,0,0.4) 0%, rgba(255,215,0,0) 70%)',\n }}\n initial={{ scale: 1.2, opacity: 0 }}\n animate={{ scale: [1.2, 1.8, 1.2], opacity: [0, 0.3, 0] }}\n transition={{ duration: 1.2, ease: 'easeInOut' }}\n />\n <motion.div\n className=\"absolute inset-0 rounded-full\"\n style={{ boxShadow: '0 0 10px 2px rgba(255,215,0,0.6)' }}\n initial={{ scale: 1, opacity: 0 }}\n animate={{ scale: [1, 1.5], opacity: [0.8, 0] }}\n transition={{ duration: 0.8, ease: 'easeOut' }}\n />\n {[...Array(6)].map((_, i) => (\n <motion.div\n key={i}\n className=\"absolute w-1 h-1 rounded-full bg-yellow-500\"\n initial={{ x: '50%', y: '50%', scale: 0, opacity: 0 }}\n animate={{\n x: `calc(50% + ${Math.cos((i * Math.PI) / 3) * 30}px)`,\n y: `calc(50% + ${Math.sin((i * Math.PI) / 3) * 30}px)`,\n scale: [0, 1, 0],\n opacity: [0, 1, 0],\n }}\n transition={{\n duration: 0.8,\n delay: i * 0.05,\n ease: 'easeOut',\n }}\n />\n ))}\n </>\n )}\n </AnimatePresence>\n </div>\n </div>\n </div>\n\n <motion.div\n className=\"absolute left-17 bottom-0 text-start flex items-center justify-center flex-col\"\n style={{ y: ySpring }}\n >\n {range.map((value) => (\n <div\n key={value}\n className=\"text-2xl font-bold flex items-center justify-start w-full\"\n style={{ height: itemHeight }}\n >\n {formatter.format(value)}\n </div>\n ))}\n </motion.div>\n </div>\n );\n}\n\nexport { StarsScrollingWheel, type StarsScrollingWheelProps };\n",
|
|
"type": "registry:ui",
|
|
"target": "components/animate-ui/components/stars-scrolling-wheel.tsx"
|
|
}
|
|
]
|
|
} |