Fortura/apps/www/public/r/pin-list.json
2025-08-20 04:12:49 -06:00

19 lines
7.0 KiB
JSON

{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "pin-list",
"type": "registry:ui",
"title": "Pin List",
"description": "Pin List Component",
"dependencies": [
"motion"
],
"registryDependencies": [],
"files": [
{
"path": "registry/components/pin-list/index.tsx",
"content": "'use client';\n\nimport * as React from 'react';\nimport { Pin } from 'lucide-react';\nimport {\n motion,\n LayoutGroup,\n AnimatePresence,\n type HTMLMotionProps,\n type Transition,\n} from 'motion/react';\nimport { cn } from '@/lib/utils';\n\ntype PinListItem = {\n id: number;\n name: string;\n info: string;\n icon: React.ElementType;\n pinned: boolean;\n};\n\ntype PinListProps = {\n items: PinListItem[];\n labels?: {\n pinned?: string;\n unpinned?: string;\n };\n transition?: Transition;\n labelMotionProps?: HTMLMotionProps<'p'>;\n className?: string;\n labelClassName?: string;\n pinnedSectionClassName?: string;\n unpinnedSectionClassName?: string;\n zIndexResetDelay?: number;\n} & HTMLMotionProps<'div'>;\n\nfunction PinList({\n items,\n labels = { pinned: 'Pinned Items', unpinned: 'All Items' },\n transition = { stiffness: 320, damping: 20, mass: 0.8, type: 'spring' },\n labelMotionProps = {\n initial: { opacity: 0 },\n animate: { opacity: 1 },\n exit: { opacity: 0 },\n transition: { duration: 0.22, ease: 'easeInOut' },\n },\n className,\n labelClassName,\n pinnedSectionClassName,\n unpinnedSectionClassName,\n zIndexResetDelay = 500,\n ...props\n}: PinListProps) {\n const [listItems, setListItems] = React.useState(items);\n const [togglingGroup, setTogglingGroup] = React.useState<\n 'pinned' | 'unpinned' | null\n >(null);\n\n const pinned = listItems.filter((u) => u.pinned);\n const unpinned = listItems.filter((u) => !u.pinned);\n\n const toggleStatus = (id: number) => {\n const item = listItems.find((u) => u.id === id);\n if (!item) return;\n\n setTogglingGroup(item.pinned ? 'pinned' : 'unpinned');\n setListItems((prev) => {\n const idx = prev.findIndex((u) => u.id === id);\n if (idx === -1) return prev;\n const updated = [...prev];\n const [item] = updated.splice(idx, 1);\n if (!item) return prev;\n const toggled = { ...item, pinned: !item.pinned };\n if (toggled.pinned) updated.push(toggled);\n else updated.unshift(toggled);\n return updated;\n });\n // Reset group z-index after the animation duration (keep in sync with animation timing)\n setTimeout(() => setTogglingGroup(null), zIndexResetDelay);\n };\n\n return (\n <motion.div className={cn('space-y-10', className)} {...props}>\n <LayoutGroup>\n <div>\n <AnimatePresence>\n {pinned.length > 0 && (\n <motion.p\n layout\n key=\"pinned-label\"\n className={cn(\n 'font-medium px-3 text-neutral-500 dark:text-neutral-300 text-sm mb-2',\n labelClassName,\n )}\n {...labelMotionProps}\n >\n {labels.pinned}\n </motion.p>\n )}\n </AnimatePresence>\n {pinned.length > 0 && (\n <div\n className={cn(\n 'space-y-3 relative',\n togglingGroup === 'pinned' ? 'z-5' : 'z-10',\n pinnedSectionClassName,\n )}\n >\n {pinned.map((item) => (\n <motion.div\n key={item.id}\n layoutId={`item-${item.id}`}\n onClick={() => toggleStatus(item.id)}\n transition={transition}\n className=\"flex items-center justify-between gap-5 rounded-2xl bg-neutral-200 dark:bg-neutral-800 p-2\"\n >\n <div className=\"flex items-center gap-2\">\n <div className=\"rounded-lg bg-background p-2\">\n <item.icon className=\"size-5 text-neutral-500 dark:text-neutral-400\" />\n </div>\n <div>\n <div className=\"text-sm font-semibold\">{item.name}</div>\n <div className=\"text-xs text-neutral-500 dark:text-neutral-400 font-medium\">\n {item.info}\n </div>\n </div>\n </div>\n <div className=\"flex items-center justify-center size-8 rounded-full bg-neutral-400 dark:bg-neutral-600\">\n <Pin className=\"size-4 text-white fill-white\" />\n </div>\n </motion.div>\n ))}\n </div>\n )}\n </div>\n\n <div>\n <AnimatePresence>\n {unpinned.length > 0 && (\n <motion.p\n layout\n key=\"all-label\"\n className={cn(\n 'font-medium px-3 text-neutral-500 dark:text-neutral-300 text-sm mb-2',\n labelClassName,\n )}\n {...labelMotionProps}\n >\n {labels.unpinned}\n </motion.p>\n )}\n </AnimatePresence>\n {unpinned.length > 0 && (\n <div\n className={cn(\n 'space-y-3 relative',\n togglingGroup === 'unpinned' ? 'z-5' : 'z-10',\n unpinnedSectionClassName,\n )}\n >\n {unpinned.map((item) => (\n <motion.div\n key={item.id}\n layoutId={`item-${item.id}`}\n onClick={() => toggleStatus(item.id)}\n transition={transition}\n className=\"flex items-center justify-between gap-5 rounded-2xl bg-neutral-200 dark:bg-neutral-800 p-2 group\"\n >\n <div className=\"flex items-center gap-2\">\n <div className=\"rounded-lg bg-background p-2\">\n <item.icon className=\"size-5 text-neutral-500 dark:text-neutral-400\" />\n </div>\n <div>\n <div className=\"text-sm font-semibold\">{item.name}</div>\n <div className=\"text-xs text-neutral-500 dark:text-neutral-400 font-medium\">\n {item.info}\n </div>\n </div>\n </div>\n <div className=\"flex items-center justify-center size-8 rounded-full bg-neutral-400 dark:bg-neutral-600 opacity-0 group-hover:opacity-100 transition-opacity duration-250\">\n <Pin className=\"size-4 text-white\" />\n </div>\n </motion.div>\n ))}\n </div>\n )}\n </div>\n </LayoutGroup>\n </motion.div>\n );\n}\n\nexport { PinList, type PinListProps, type PinListItem };\n",
"type": "registry:ui",
"target": "components/animate-ui/components/pin-list.tsx"
}
]
}