{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "author": "DesignX ", "name": "motion-breadcrumb", "type": "registry:component", "title": "Breadcrumb", "description": "Breadcrumbs that collapse and expand with motion.", "dependencies": [ "lucide-react@^0.577.0", "motion@^14.0.0" ], "registryDependencies": [ "@dx/lib-ease", "@dx/popover-morph", "@dx/utils" ], "files": [ { "path": "components/motion/breadcrumb.tsx", "type": "registry:component", "target": "components/motion/breadcrumb.tsx", "content": "import { ChevronRight, Ellipsis } from \"lucide-react\";\nimport {\n AnimatePresence,\n LayoutGroup,\n motion,\n useIsPresent,\n useReducedMotion,\n type HTMLMotionProps,\n} from \"motion/react\";\nimport {\n Children,\n forwardRef,\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n type ReactNode,\n useId,\n type ComponentPropsWithRef,\n type ReactElement,\n} from \"react\";\nimport { MorphPopover, MorphPopoverContent, MorphPopoverTrigger } from \"@/components/motion/popover-morph\";\nimport { useHoverCapable } from \"@/lib/hooks/use-hover-capable\";\nimport { EASE_OUT, SPRING_LAYOUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport type BreadcrumbProps = ComponentPropsWithRef<\"nav\">;\n\n/** A navigation landmark. Keep it mounted while the route changes. */\nexport function Breadcrumb({ className, children, ...props }: BreadcrumbProps) {\n const id = useId();\n return (\n \n );\n}\n\nexport type BreadcrumbListProps = ComponentPropsWithRef<\"ol\"> & {\n /** Maximum visible slots, including the ellipsis. Minimum 3; Infinity disables collapsing. */\n maxItems?: number;\n /** Accessible label for the hidden ancestor disclosure. */\n overflowLabel?: string;\n};\n\n/** Pass keyed BreadcrumbItems directly so entering and leaving routes animate. */\nexport function BreadcrumbList({ className, children, maxItems = 4, overflowLabel = \"Show hidden paths\", ...props }: BreadcrumbListProps) {\n const items = Children.toArray(children);\n const limit = Number.isFinite(maxItems) ? Math.max(3, Math.floor(maxItems)) : 4;\n const collapse = maxItems !== Infinity && items.length > limit;\n const tailCount = limit - 2;\n const visible = collapse ? [\n items[0],\n \n \n \n {items.slice(1, -tailCount)}\n \n ,\n ...items.slice(-tailCount),\n ] : items;\n return (\n \n {visible}\n \n );\n}\n\nexport type BreadcrumbItemProps = HTMLMotionProps<\"li\">;\n\n/** Use a stable route key; put its optional separator inside this item. */\nexport const BreadcrumbItem = forwardRef(\n function BreadcrumbItem({ className, style, children, ...props }, ref) {\n const reduce = useReducedMotion();\n const present = useIsPresent();\n const itemRef = useRef(null);\n useLayoutEffect(() => {\n const item = itemRef.current;\n if (!item || !present) return;\n const measure = () => {\n // popLayout snapshots offsetWidth (integer pixels). Retain the exact\n // width so a fractional-pixel loss cannot wrap the final character.\n item.style.setProperty(\"--breadcrumb-exit-width\", `${item.getBoundingClientRect().width}px`);\n };\n measure();\n const observer = new ResizeObserver(measure);\n observer.observe(item);\n return () => observer.disconnect();\n }, [present]);\n const hidden = { opacity: 0, y: reduce ? 0 : 6 };\n return (\n {\n itemRef.current = node;\n if (typeof ref === \"function\") return ref(node);\n if (ref) ref.current = node;\n }}\n layout={reduce ? false : \"position\"}\n initial={hidden}\n animate={{ opacity: 1, y: 0 }}\n exit={hidden}\n transition={{ duration: 0.2, ease: EASE_OUT, layout: SPRING_LAYOUT }}\n {...props}\n inert={!present}\n aria-hidden={!present || undefined}\n style={{\n ...style,\n minWidth: present ? style?.minWidth : \"var(--breadcrumb-exit-width)\",\n pointerEvents: present ? style?.pointerEvents : \"none\",\n }}\n className={cn(\"relative inline-flex min-w-0 max-w-full items-center gap-1\", className)}\n >\n {children}\n \n );\n },\n);\n\nexport type BreadcrumbLinkProps = ComponentPropsWithRef<\"a\"> & {\n /** Render your router's Link, spreading these props onto it. */\n render?: (props: ComponentPropsWithRef<\"a\">) => ReactElement;\n};\n\nexport function BreadcrumbLink({ className, render, ...props }: BreadcrumbLinkProps) {\n const linkProps = {\n ...props,\n className: cn(\n \"inline-flex min-h-8 min-w-0 items-center gap-1.5 rounded-md px-2 font-medium text-muted-foreground transition-colors duration-150 hover:bg-muted/60 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 [&>svg]:size-3.5 [&>svg]:shrink-0\",\n className,\n ),\n };\n // oxlint-disable-next-line jsx-a11y/anchor-has-content\n return render ? render(linkProps) : ;\n}\n\nexport type BreadcrumbPageProps = ComponentPropsWithRef<\"span\">;\n\nexport function BreadcrumbPage({ className, children, ...props }: BreadcrumbPageProps) {\n return (\n svg]:size-3.5 [&>svg]:shrink-0\",\n className,\n )}\n >\n {children}\n \n );\n}\n\nexport type BreadcrumbSeparatorProps = ComponentPropsWithRef<\"span\">;\n\n/** Decorative separator, placed inside the following BreadcrumbItem. */\nexport function BreadcrumbSeparator({ className, children, ...props }: BreadcrumbSeparatorProps) {\n return (\n svg]:size-3.5 rtl:rotate-180\", className)}\n >\n {children ?? }\n \n );\n}\n\n\nexport interface BreadcrumbEllipsisProps {\n /** Hidden BreadcrumbItems, in path order. */\n children: ReactNode;\n className?: string;\n label?: string;\n}\n\n/** Hover disclosure with click/touch toggle and keyboard access to ancestor links. */\nexport function BreadcrumbEllipsis({ children, className, label = \"Show hidden paths\" }: BreadcrumbEllipsisProps) {\n const [open, setOpen] = useState(false);\n const [placement, setPlacement] = useState<{ align: \"start\" | \"end\"; side: \"top\" | \"bottom\"; width: number }>({ align: \"start\", side: \"bottom\", width: 224 });\n const canHover = useHoverCapable();\n const present = useIsPresent();\n const trigger = useRef(null);\n const panel = useRef(null);\n const focusOnOpen = useRef(false);\n const closeTimer = useRef | null>(null);\n\n useLayoutEffect(() => {\n if (!open) return;\n const update = () => {\n const rect = trigger.current?.getBoundingClientRect();\n if (!rect) return;\n const right = window.innerWidth - rect.left - 8;\n const left = rect.right - 8;\n const align = right < 224 && left > right ? \"end\" : \"start\";\n const below = window.innerHeight - rect.bottom;\n setPlacement({\n align,\n side: below < 280 && rect.top > below ? \"top\" : \"bottom\",\n width: Math.max(32, Math.min(224, align === \"start\" ? right : left)),\n });\n };\n update();\n window.addEventListener(\"resize\", update);\n return () => window.removeEventListener(\"resize\", update);\n }, [open]);\n\n const cancelClose = () => {\n if (closeTimer.current !== null) clearTimeout(closeTimer.current);\n closeTimer.current = null;\n };\n const leave = () => {\n cancelClose();\n // Allow the pointer to cross the gap between the trigger and portal.\n closeTimer.current = setTimeout(() => {\n if (!panel.current?.contains(document.activeElement) && document.activeElement !== trigger.current) setOpen(false);\n }, 160);\n };\n useEffect(() => () => {\n if (closeTimer.current !== null) clearTimeout(closeTimer.current);\n }, []);\n\n useEffect(() => {\n if (!open) return;\n const onFocus = (event: FocusEvent) => {\n if (event.target instanceof Node && event.target !== trigger.current && !panel.current?.contains(event.target)) setOpen(false);\n };\n document.addEventListener(\"focusin\", onFocus);\n return () => document.removeEventListener(\"focusin\", onFocus);\n }, [open]);\n\n return (\n \n {\n cancelClose();\n if (canHover && event.pointerType === \"mouse\") {\n focusOnOpen.current = false;\n setOpen(true);\n }\n }}\n onPointerLeave={leave}\n >\n \n { focusOnOpen.current = event.detail === 0; }}\n onKeyDown={(event) => {\n if (event.key === \"ArrowDown\") {\n event.preventDefault();\n focusOnOpen.current = true;\n setOpen(true);\n panel.current?.querySelector(\"a[href],button\")?.focus();\n }\n }}\n >\n \n \n \n \n { cancelClose(); setOpen(true); }}\n onPointerLeave={leave}\n onClick={(event) => {\n if ((event.target as Element).closest(\"a[href]\")) setOpen(false);\n }}\n >\n {children}\n \n \n \n \n );\n}\n\nfunction BreadcrumbOverflowPaths({ focusOnOpen, ref, ...props }: ComponentPropsWithRef<\"ol\"> & { focusOnOpen: { current: boolean } }) {\n const localRef = useRef(null);\n useLayoutEffect(() => {\n if (!focusOnOpen.current) return;\n const focus = () => localRef.current?.querySelector(\"a[href],button\")?.focus();\n focus();\n // The portal becomes visible after its parent's layout measurement.\n const frame = requestAnimationFrame(() => {\n focus();\n focusOnOpen.current = false;\n });\n return () => cancelAnimationFrame(frame);\n }, [focusOnOpen]);\n return (\n {\n localRef.current = node;\n if (typeof ref === \"function\") return ref(node);\n if (ref) ref.current = node;\n }}\n className=\"flex max-h-64 flex-col gap-0.5 overflow-y-auto [&>li]:w-full [&_a]:w-full [&_a]:py-1 [&_a]:[overflow-wrap:anywhere] [&_[data-breadcrumb-separator]]:hidden\"\n />\n );\n}\n" } ], "categories": [ "motion-kit" ], "meta": { "group": "motion", "docs": "https://dxuireact.com/docs/components/motion-breadcrumb" } }