{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "interactive-card",
  "type": "registry:ui",
  "title": "Interactive Card",
  "description": "The whole card is one link, while buttons inside still get their own clicks.",
  "categories": [
    "disclosure"
  ],
  "dependencies": [],
  "registryDependencies": [
    "https://www.stealth.pm/r/tokens.json",
    "https://www.stealth.pm/r/cn.json"
  ],
  "files": [
    {
      "path": "components/ui/interactive-card.tsx",
      "content": "\"use client\";\nimport { cloneElement, createContext, isValidElement, use, useRef } from \"react\";\nimport { cn } from \"@/lib/cn\";\n\nconst CardContext = createContext<{ disabled: boolean }>({ disabled: false });\n\nexport type InteractiveCardProps = React.ComponentProps<\"div\"> & {\n  /** A faint light that follows the mouse across the surface. Mouse only; off under reduced motion. */\n  glow?: boolean;\n  /** Dims the card and takes its link out of the tab order. Nested actions keep their own state. */\n  disabled?: boolean;\n};\n\n/**\n * A card whose whole surface is one link. The link's ::after stretches over the\n * card, so the target is the card while the name is still the link's only text;\n * nested controls sit above that layer in InteractiveCardAction.\n */\nexport function InteractiveCard({ glow = false, disabled = false, className, children, onPointerMove, onPointerLeave, ...rest }: InteractiveCardProps) {\n  const frame = useRef(0);\n\n  return (\n    <CardContext value={{ disabled }}>\n      <div\n        data-disabled={disabled || undefined}\n        data-glow={glow || undefined}\n        onPointerMove={(e) => {\n          onPointerMove?.(e);\n          if (!glow || e.pointerType !== \"mouse\") return;\n          const el = e.currentTarget;\n          const { clientX, clientY } = e;\n          // Written straight to CSS variables, once per frame, so following the mouse never re-renders.\n          cancelAnimationFrame(frame.current);\n          frame.current = requestAnimationFrame(() => {\n            const r = el.getBoundingClientRect();\n            el.style.setProperty(\"--glow-x\", `${clientX - r.left}px`);\n            el.style.setProperty(\"--glow-y\", `${clientY - r.top}px`);\n          });\n        }}\n        onPointerLeave={(e) => {\n          onPointerLeave?.(e);\n          cancelAnimationFrame(frame.current);\n        }}\n        className={cn(\n          \"group/card relative isolate flex min-w-0 flex-col rounded-xl border border-line-2 bg-raised shadow-[var(--shadow)]\",\n          \"transition-[border-color,background-color,scale] duration-150 ease-out\",\n          \"hover:border-fg-4 data-disabled:hover:border-line-2\",\n          // Press feedback belongs to the link only; pressing a nested action leaves the card still.\n          \"has-[[data-card-link]:active]:scale-[0.985] has-[[data-card-link]:active]:duration-100 motion-reduce:has-[[data-card-link]:active]:scale-100\",\n          // The ring draws around the card, because the card is what the link opens.\n          \"has-[[data-card-link]:focus-visible]:outline-solid has-[[data-card-link]:focus-visible]:outline-1 has-[[data-card-link]:focus-visible]:outline-offset-2 has-[[data-card-link]:focus-visible]:outline-fg-3\",\n          \"data-disabled:opacity-60\",\n          glow &&\n            cn(\n              \"before:pointer-events-none before:absolute before:inset-0 before:-z-10 before:rounded-[inherit] before:opacity-0 before:content-['']\",\n              \"before:bg-[radial-gradient(260px_circle_at_var(--glow-x,50%)_var(--glow-y,0px),color-mix(in_oklab,var(--fg)_6%,transparent),transparent_70%)]\",\n              \"before:transition-opacity before:duration-300 hover:before:opacity-100 data-disabled:before:hidden motion-reduce:before:hidden\",\n            ),\n          className,\n        )}\n        {...rest}\n      >\n        {children}\n      </div>\n    </CardContext>\n  );\n}\n\nexport type InteractiveCardLinkProps = React.ComponentProps<\"a\"> & {\n  /** Render your router's link instead of an <a>: `render={<Link href=\"/projects/web\" />}`. */\n  render?: React.ReactElement<{ className?: string; children?: React.ReactNode }>;\n};\n\n/** The card's one destination. Put the card's name in it, nothing else. */\nexport function InteractiveCardLink({ render, className, children, onClick, ...rest }: InteractiveCardLinkProps) {\n  const { disabled } = use(CardContext);\n  const classes = cn(\n    \"rounded-sm text-fg outline-none\",\n    // The stretched hit area, with the card's radius so the corners press as they look.\n    \"after:absolute after:inset-0 after:z-0 after:rounded-xl after:content-['']\",\n    \"[-webkit-tap-highlight-color:transparent] touch-manipulation\",\n    disabled ? \"cursor-not-allowed after:cursor-not-allowed\" : \"cursor-pointer\",\n    className,\n  );\n  const shared = {\n    \"data-card-link\": \"\",\n    \"aria-disabled\": disabled || undefined,\n    tabIndex: disabled ? -1 : undefined,\n    onClick: (e: React.MouseEvent<HTMLAnchorElement>) => {\n      if (disabled) {\n        e.preventDefault();\n        return;\n      }\n      onClick?.(e);\n    },\n  };\n\n  if (render && isValidElement(render)) {\n    return cloneElement(render, { ...rest, ...shared, className: cn(classes, render.props.className), children } as React.HTMLAttributes<HTMLElement>);\n  }\n  return (\n    <a className={classes} {...rest} {...shared}>\n      {children}\n    </a>\n  );\n}\n\nexport type InteractiveCardActionProps = React.ComponentProps<\"div\">;\n\n/** Lifts buttons, menus and secondary links above the card's link so they get their own clicks. */\nexport function InteractiveCardAction({ className, ...rest }: InteractiveCardActionProps) {\n  return <div className={cn(\"relative z-10 flex items-center\", className)} {...rest} />;\n}\n\nexport type InteractiveCardArrowProps = React.ComponentProps<\"svg\"> & {\n  /** Points right for a page in this app, up-right for somewhere else. */\n  external?: boolean;\n};\n\n/** A quiet arrow that leans toward the destination while the card is hovered. */\nexport function InteractiveCardArrow({ external = false, className, ...rest }: InteractiveCardArrowProps) {\n  return (\n    <svg\n      width=\"16\"\n      height=\"16\"\n      viewBox=\"0 0 16 16\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={1.4}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      aria-hidden\n      className={cn(\n        \"shrink-0 text-fg-4 transition-[translate,color] duration-200 ease-out-quart group-hover/card:text-fg-2 motion-reduce:transition-colors\",\n        external ? \"group-hover/card:translate-x-0.5 group-hover/card:-translate-y-0.5\" : \"group-hover/card:translate-x-0.5\",\n        \"group-data-disabled/card:translate-0! group-data-disabled/card:text-fg-4!\",\n        className,\n      )}\n      {...rest}\n    >\n      {external ? <path d=\"M5 11 11 5M6 5h5v5\" /> : <path d=\"M3 8h10M9 4l4 4-4 4\" />}\n    </svg>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/interactive-card"
}
