{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "author": "DesignX ", "name": "color-list", "type": "registry:component", "title": "Color List", "description": "A scroll list where the focused row washes the background in its colour.", "dependencies": [ "motion@^14.0.0" ], "registryDependencies": [ "@dx/utils" ], "files": [ { "path": "dx/color-list.tsx", "type": "registry:component", "target": "components/dx/color-list.tsx", "content": "import * as React from \"react\";\nimport { AnimatePresence, motion } from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/**\n * ColorLista scrolling index where the row crossing the focus line takes\n * over: the background washes to its colour, the row lights up and a\n * floating preview swaps to its media. Optionally loops forever.\n */\n\ntype ColorListItem = {\n id: string;\n name: string;\n badge?: string;\n meta?: string;\n /** Background colour while active. Hex colours pick a readable text colour automatically. */\n color: string;\n /** Text colour override. */\n foreground?: string;\n /** Image URL or any CSS background for the preview tile. */\n preview?: string;\n};\n\ntype ColorListProps = {\n items: ColorListItem[];\n /** Repeat the list and keep appending as you near the end. */\n infinite?: boolean;\n /** Where the focus line sits, as a fraction of the viewport height. */\n focusLine?: number;\n showPreview?: boolean;\n previewSize?: \"sm\" | \"md\" | \"lg\";\n /** Fires when the active row changeshook a tick sound or analytics here. */\n onActiveChange?: (item: ColorListItem, index: number) => void;\n className?: string;\n itemClassName?: string;\n};\n\nfunction readable(color: string) {\n const m = /^#?([\\da-f]{3}|[\\da-f]{6})$/i.exec(color.trim());\n if (!m) return \"#121317\";\n let hex = m[1];\n if (hex.length === 3) hex = [...hex].map((c) => c + c).join(\"\");\n const [r, g, b] = [0, 2, 4].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255);\n const lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;\n return lum > 0.55 ? \"#121317\" : \"#ffffff\";\n}\n\nconst SIZES = { sm: \"size-40\", md: \"size-56\", lg: \"size-72\" };\n\nfunction ColorList({\n items,\n infinite = true,\n focusLine = 0.35,\n showPreview = true,\n previewSize = \"md\",\n onActiveChange,\n className,\n itemClassName,\n}: ColorListProps) {\n const root = React.useRef(null);\n const rows = React.useRef<(HTMLDivElement | null)[]>([]);\n const [sets, setSets] = React.useState(infinite ? 3 : 1);\n const [active, setActive] = React.useState(0);\n const onChange = React.useRef(onActiveChange);\n onChange.current = onActiveChange;\n\n const total = items.length * sets;\n const current = items[active % items.length];\n const fg = current?.foreground ?? readable(current?.color ?? \"\");\n\n const first = React.useRef(true);\n React.useEffect(() => {\n if (first.current) {\n first.current = false;\n return;\n }\n onChange.current?.(items[active % items.length], active);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [active]);\n\n React.useEffect(() => {\n const el = root.current;\n if (!el) return;\n const top = Math.round(focusLine * 100);\n const io = new IntersectionObserver(\n (entries) => {\n for (const e of entries) {\n if (!e.isIntersecting) continue;\n const i = Number((e.target as HTMLElement).dataset.index);\n setActive(i);\n }\n },\n { root: el, rootMargin: `-${top}% 0px -${100 - top - 1}% 0px`, threshold: 0 },\n );\n rows.current.slice(0, total).forEach((r) => r && io.observe(r));\n return () => io.disconnect();\n }, [items, total, focusLine]);\n\n const onScroll = () => {\n const el = root.current;\n if (!infinite || !el) return;\n if (el.scrollTop + el.clientHeight > el.scrollHeight - el.clientHeight) setSets((s) => s + 2);\n };\n\n return (\n \n
\n
\n
\n {Array.from({ length: total }, (_, i) => {\n const item = items[i % items.length];\n const on = i === active;\n return (\n {\n rows.current[i] = el;\n }}\n data-index={i}\n data-active={on || undefined}\n className={cn(\n \"flex items-center gap-6 px-6 py-2 transition-opacity duration-300 md:px-10\",\n on ? \"opacity-100\" : \"opacity-25 hover:opacity-60\",\n itemClassName,\n )}\n >\n {item.name}\n {item.badge && {item.badge}}\n {item.meta && }\n
\n );\n })}\n
\n
\n
\n\n {showPreview && current && (\n \n )}\n \n );\n}\n\nexport { ColorList };\nexport type { ColorListItem, ColorListProps };\n" } ], "categories": [ "motion" ], "meta": { "group": "dx", "docs": "https://dxuireact.com/docs/components/color-list" } }