{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"not-found-magnetic","type":"registry:block","title":"404 / Not Found Magnetic","description":"Each digit is cursor-attracted via the Magnetic wrapper and springs back on leave.","author":"Saurabh <saurabh10102@gmail.com>","dependencies":["clsx","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/not-found/magnetic.tsx","type":"registry:component","target":"@components/motion/not-found/magnetic.tsx","content":"\"use client\";\n// beui.dev/components/blocks/not-found\n\nimport { Magnetic } from \"@/components/motion/magnetic\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  NOT_FOUND_DEFAULTS,\n  NotFoundActions,\n  NotFoundStage,\n  type NotFoundProps,\n} from \"./shared\";\n\nexport function NotFoundMagnetic({\n  className,\n  code = NOT_FOUND_DEFAULTS.code,\n  title = NOT_FOUND_DEFAULTS.title,\n  description = NOT_FOUND_DEFAULTS.description,\n  homeHref,\n  homeLabel,\n  browseHref,\n  browseLabel,\n}: NotFoundProps) {\n  const chars = code.split(\"\");\n\n  return (\n    <NotFoundStage className={className}>\n      <h1\n        aria-label={code}\n        className=\"flex select-none items-center justify-center font-bold leading-none tracking-tighter text-foreground [font-size:clamp(5rem,18vw,12rem)]\"\n      >\n        {chars.map((ch, i) => (\n          <Magnetic\n            // biome-ignore lint/suspicious/noArrayIndexKey: fixed positional glyphs\n            key={i}\n            strength={0.6}\n            className={cn(i > 0 && \"-ml-2\")}\n          >\n            <span aria-hidden className=\"inline-block px-1 tabular-nums\">\n              {ch}\n            </span>\n          </Magnetic>\n        ))}\n      </h1>\n\n      <div className=\"flex flex-col items-center gap-2\">\n        <p className=\"text-lg font-semibold text-foreground\">{title}</p>\n        <p className=\"max-w-sm text-sm text-muted-foreground\">{description}</p>\n      </div>\n\n      <NotFoundActions\n        homeHref={homeHref}\n        homeLabel={homeLabel}\n        browseHref={browseHref}\n        browseLabel={browseLabel}\n      />\n    </NotFoundStage>\n  );\n}\n"},{"path":"components/motion/not-found/shared.tsx","type":"registry:component","target":"@components/motion/not-found/shared.tsx","content":"\"use client\";\n\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { SPRING_PRESS } from \"@/lib/ease\";\nimport { useHoverCapable } from \"@/lib/hooks/use-hover-capable\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface NotFoundProps {\n  className?: string;\n  /** The big status code. */\n  code?: string;\n  title?: string;\n  description?: string;\n  homeHref?: string;\n  homeLabel?: string;\n  browseHref?: string;\n  browseLabel?: string;\n}\n\nexport const NOT_FOUND_DEFAULTS = {\n  code: \"404\",\n  title: \"Page not found\",\n  description:\n    \"The page you are looking for moved, vanished, or never existed.\",\n  homeHref: \"/\",\n  homeLabel: \"Back home\",\n  browseHref: \"/components/motion\",\n  browseLabel: \"Browse components\",\n} as const;\n\ntype ActionsProps = Pick<\n  NotFoundProps,\n  \"homeHref\" | \"homeLabel\" | \"browseHref\" | \"browseLabel\" | \"className\"\n>;\n\n/** The shared dual CTA: a primary \"Back home\" and a secondary \"Browse\". */\nexport function NotFoundActions({\n  homeHref = NOT_FOUND_DEFAULTS.homeHref,\n  homeLabel = NOT_FOUND_DEFAULTS.homeLabel,\n  browseHref = NOT_FOUND_DEFAULTS.browseHref,\n  browseLabel = NOT_FOUND_DEFAULTS.browseLabel,\n  className,\n}: ActionsProps) {\n  const reduce = useReducedMotion();\n  const canHover = useHoverCapable();\n  const whileTap = reduce ? undefined : { scale: 0.96 };\n  const whileHover = reduce || !canHover ? undefined : { scale: 1.02 };\n\n  return (\n    <div\n      className={cn(\n        \"flex flex-wrap items-center justify-center gap-3\",\n        className,\n      )}\n    >\n      <motion.a\n        href={homeHref}\n        whileTap={whileTap}\n        whileHover={whileHover}\n        transition={SPRING_PRESS}\n        className=\"inline-flex h-11 select-none items-center justify-center rounded-full bg-primary px-6 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90\"\n      >\n        {homeLabel}\n      </motion.a>\n      <motion.a\n        href={browseHref}\n        whileTap={whileTap}\n        whileHover={whileHover}\n        transition={SPRING_PRESS}\n        className=\"inline-flex h-11 select-none items-center justify-center rounded-full border border-border bg-card px-6 text-sm font-medium text-foreground transition-colors hover:bg-primary/5\"\n      >\n        {browseLabel}\n      </motion.a>\n    </div>\n  );\n}\n\n/** Centers a variant and gives it a consistent minimum stage height. */\nexport function NotFoundStage({\n  className,\n  children,\n}: {\n  className?: string;\n  children: React.ReactNode;\n}) {\n  return (\n    <div\n      className={cn(\n        \"flex min-h-[420px] w-full flex-col items-center justify-center gap-8 px-4 text-center\",\n        className,\n      )}\n    >\n      {children}\n    </div>\n  );\n}\n"},{"path":"components/motion/magnetic.tsx","type":"registry:component","target":"@components/motion/magnetic.tsx","content":"\"use client\";\n\nimport { motion, useMotionValue, useReducedMotion, useSpring } from \"motion/react\";\nimport { useRef, type ReactNode } from \"react\";\nimport { SPRING_MOUSE } from \"@/lib/ease\";\nimport { useHoverCapable } from \"@/lib/hooks/use-hover-capable\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface MagneticProps {\n  children: ReactNode;\n  strength?: number;\n  className?: string;\n}\n\nexport function Magnetic({ children, strength = 0.35, className }: MagneticProps) {\n  const ref = useRef<HTMLDivElement>(null);\n  const reduce = useReducedMotion();\n  const canHover = useHoverCapable();\n  // Decorative cursor-follow: skip on touch (phantom hover) and reduced motion.\n  const enabled = !reduce && canHover;\n  const x = useMotionValue(0);\n  const y = useMotionValue(0);\n  const sx = useSpring(x, SPRING_MOUSE);\n  const sy = useSpring(y, SPRING_MOUSE);\n\n  const onMove = (e: React.MouseEvent<HTMLDivElement>) => {\n    const el = ref.current;\n    if (!el || !enabled) return;\n    const rect = el.getBoundingClientRect();\n    x.set((e.clientX - rect.left - rect.width / 2) * strength);\n    y.set((e.clientY - rect.top - rect.height / 2) * strength);\n  };\n\n  const onLeave = () => {\n    x.set(0);\n    y.set(0);\n  };\n\n  return (\n    <motion.div\n      ref={ref}\n      onMouseMove={onMove}\n      onMouseLeave={onLeave}\n      style={{ x: sx, y: sy }}\n      className={cn(\"inline-block\", className)}\n    >\n      {children}\n    </motion.div>\n  );\n}\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"},{"path":"lib/ease.ts","type":"registry:lib","target":"@lib/ease.ts","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n\n/** Dragged handles and fills (sliders) — critically damped `useSpring` config,\n * so the value follows the pointer butterily and never rebounds off an end. */\nexport const SPRING_GLIDE = {\n  stiffness: 700,\n  damping: 50,\n  mass: 0.5,\n} as const;\n"},{"path":"lib/hooks/use-hover-capable.ts","type":"registry:hook","target":"@lib/hooks/use-hover-capable.ts","content":"\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\n/**\n * Returns true only on devices that have a true hover (mouse / trackpad).\n * Touch devices fire phantom `:hover` on tap that sticks until tap-elsewhere\n * — gate hover-only effects (scale lifts, magnetic pulls) behind this.\n */\nexport function useHoverCapable() {\n  const [canHover, setCanHover] = useState(false);\n\n  useEffect(() => {\n    if (typeof window === \"undefined\" || !window.matchMedia) return;\n    const mq = window.matchMedia(\"(hover: hover) and (pointer: fine)\");\n    const update = () => setCanHover(mq.matches);\n    update();\n    mq.addEventListener?.(\"change\", update);\n    return () => mq.removeEventListener?.(\"change\", update);\n  }, []);\n\n  return canHover;\n}\n"}]}