{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "3d-pin",
  "title": "3D Pin",
  "description": "Card that tilts back as a glowing pin rises with a label chip and rippling rings.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/velora/3d-pin.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\ninterface Pin3DProps\n  extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, \"title\"> {\n  /** Label shown in the chip at the top of the pin */\n  title: string;\n  /** Makes the whole card a link; without it the root is a plain div */\n  href?: string;\n  /** Classes for the card surface that holds the children */\n  cardClassName?: string;\n  /** Card content, drawn on the tilting surface */\n  children: React.ReactNode;\n}\n\nconst RINGS = [0, 1, 2];\n\n/**\n * Card that tilts back in perspective on hover or keyboard focus while a\n * glowing pin rises from its centre, topped by a `title` chip, with rings\n * rippling out across the card at the pin's base. Pass `href` to make the\n * whole card a link. Leave room above the card for the pin (about 1.5rem).\n */\nexport function Pin3D({\n  title,\n  href,\n  cardClassName,\n  className,\n  children,\n  ...props\n}: Pin3DProps) {\n  const [hovered, setHovered] = useState(false);\n  const [focused, setFocused] = useState(false);\n  const rings = useRef<(HTMLSpanElement | null)[]>([]);\n  const active = hovered || focused;\n\n  useEffect(() => {\n    if (!active || matchMedia(\"(prefers-reduced-motion: reduce)\").matches) return;\n    const animations = rings.current.map((ring, i) =>\n      ring?.animate(\n        [\n          { transform: \"scale(0.1)\", opacity: 0 },\n          { opacity: 1, offset: 0.25 },\n          { transform: \"scale(1)\", opacity: 0 },\n        ],\n        { duration: 3600, delay: i * 1200, iterations: Infinity, easing: \"ease-out\", fill: \"backwards\" }\n      )\n    );\n    return () => animations.forEach((a) => a?.cancel());\n  }, [active]);\n\n  const Root = (href ? \"a\" : \"div\") as \"a\";\n\n  return (\n    <Root\n      {...props}\n      href={href}\n      data-slot=\"3d-pin\"\n      data-active={active || undefined}\n      className={cn(\n        \"group/pin relative block outline-none perspective-[1000px]\",\n        className\n      )}\n      onPointerEnter={(e) => {\n        setHovered(true);\n        props.onPointerEnter?.(e);\n      }}\n      onPointerLeave={(e) => {\n        setHovered(false);\n        props.onPointerLeave?.(e);\n      }}\n      onFocus={(e) => {\n        if (e.target.matches(\":focus-visible\")) setFocused(true);\n        props.onFocus?.(e);\n      }}\n      onBlur={(e) => {\n        if (!e.currentTarget.contains(e.relatedTarget)) setFocused(false);\n        props.onBlur?.(e);\n      }}\n    >\n      <div className=\"relative transform-3d transition-transform duration-700 ease-[cubic-bezier(0.2,0.8,0.2,1)] motion-reduce:transition-none motion-safe:group-data-active/pin:scale-84 motion-safe:group-data-active/pin:rotate-x-40\">\n        <div\n          className={cn(\n            \"relative overflow-hidden rounded-2xl border bg-card p-3 text-card-foreground shadow-sm transition-shadow duration-700 group-focus-visible/pin:ring-2 group-focus-visible/pin:ring-ring group-focus-visible/pin:ring-offset-2 group-focus-visible/pin:ring-offset-background group-data-active/pin:shadow-2xl group-data-active/pin:shadow-brand/15\",\n            cardClassName\n          )}\n        >\n          {children}\n        </div>\n        <span\n          aria-hidden\n          className=\"pointer-events-none absolute top-1/2 left-1/2 opacity-0 transition-opacity duration-500 group-data-active/pin:opacity-100 motion-reduce:transition-none\"\n        >\n          {RINGS.map((i) => (\n            <span\n              key={i}\n              ref={(el) => {\n                rings.current[i] = el;\n              }}\n              style={{ transform: `scale(${0.3 + i * 0.2})`, opacity: 0.7 - i * 0.2 }}\n              className=\"absolute -top-24 -left-24 size-48 rounded-full border border-brand/60 bg-brand/5 shadow-[0_0_28px_-6px_var(--brand)]\"\n            />\n          ))}\n        </span>\n      </div>\n\n      <span className=\"pointer-events-none absolute inset-x-0 -top-5 bottom-1/2 z-10 flex flex-col items-center opacity-0 transition duration-500 group-data-active/pin:opacity-100 motion-safe:translate-y-4 motion-safe:group-data-active/pin:translate-y-0 motion-safe:group-data-active/pin:delay-150 motion-reduce:transition-none\">\n        <span className=\"relative flex items-center gap-1.5 rounded-full border bg-background/85 px-3 py-1 text-xs font-medium whitespace-nowrap text-foreground shadow-lg shadow-brand/10 backdrop-blur\">\n          <span aria-hidden className=\"size-1.5 rounded-full bg-brand\" />\n          {title}\n          <span\n            aria-hidden\n            className=\"absolute inset-x-3 -bottom-px h-px bg-linear-to-r from-transparent via-brand to-transparent\"\n          />\n        </span>\n        <span\n          aria-hidden\n          className=\"relative w-px flex-1 origin-bottom bg-linear-to-b from-transparent via-brand/60 to-brand transition-transform duration-700 motion-safe:scale-y-0 motion-safe:group-data-active/pin:scale-y-100 motion-reduce:transition-none\"\n        >\n          <span className=\"absolute inset-y-0 -left-px w-[3px] [background:inherit] blur-[2px]\" />\n        </span>\n        <span\n          aria-hidden\n          className=\"-mb-1 size-2 rounded-full bg-brand shadow-[0_0_12px_2px_var(--brand)] ring-4 ring-brand/20\"\n        />\n      </span>\n    </Root>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/velora/3d-pin.tsx"
    }
  ],
  "type": "registry:ui"
}