{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "notch-card",
  "title": "Notch card",
  "description": "A specimen card with a notch cut from one corner; its image blooms from grey to colour on hover or focus, with one unbroken hairline.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/manniche/notch-card/notch-card.tsx",
      "content": "import { useEffect, useLayoutEffect, useRef, type AnchorHTMLAttributes, type HTMLAttributes, type ReactNode } from 'react'\nimport { ArrowUpRight } from 'lucide-react'\nimport { cn } from '@/lib/utils'\n\nexport type NotchCardProps = Omit<HTMLAttributes<HTMLDivElement>, 'title'> & {\n  /** The picture at the top: an img or an SVG. It fills the frame and is shown in grey until it blooms. */\n  image: ReactNode\n  /** The card's title. With `href` it is the link, stretched over the whole card. */\n  title: ReactNode\n  /** A small mono line above the title. */\n  eyebrow?: ReactNode\n  /** A short caption under the title. */\n  children?: ReactNode\n  /** A row at the bottom, under a hairline, e.g. two small facts. */\n  footer?: ReactNode\n  /** Makes the whole card a link. */\n  href?: string\n  /** Extra attributes for the link, e.g. `target` or `onClick`. */\n  linkProps?: Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href' | 'children'>\n  /** What sits in the notch. Pass your own button for a second action; with `href` the default is an arrow. */\n  action?: ReactNode\n  /** Which top corner the notch is cut from. */\n  notch?: 'top-right' | 'top-left'\n  /** Controlled: show the image in colour, e.g. for a selected card. */\n  bloom?: boolean\n}\n\nconst R = 26 // corner radius, the tile radius\nconst N = 56 // notch size\nconst r = 14 // radius where the notch turns, the inner radius\n\n// The card's outline with the notch cut from a top corner, in pixels. The left one is the right one mirrored.\nfunction outline(W: number, H: number, left: boolean) {\n  if (left)\n    return `M${W - R} 0H${N + r}A${r} ${r} 0 0 0 ${N} ${r}V${N - r}A${r} ${r} 0 0 1 ${N - r} ${N}H${R}A${R} ${R} 0 0 0 0 ${N + R}V${H - R}A${R} ${R} 0 0 0 ${R} ${H}H${W - R}A${R} ${R} 0 0 0 ${W} ${H - R}V${R}A${R} ${R} 0 0 0 ${W - R} 0Z`\n  return `M${R} 0H${W - N - r}A${r} ${r} 0 0 1 ${W - N} ${r}V${N - r}A${r} ${r} 0 0 0 ${W - N + r} ${N}H${W - R}A${R} ${R} 0 0 1 ${W} ${N + R}V${H - R}A${R} ${R} 0 0 1 ${W - R} ${H}H${R}A${R} ${R} 0 0 1 0 ${H - R}V${R}A${R} ${R} 0 0 1 ${R} 0Z`\n}\n\nconst useIsoLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect\n\n/**\n * A card with a notch cut from one top corner, like a punched specimen card; the notch holds its action.\n * The image is grey at rest and blooms into colour on hover or keyboard focus. Only filter and transform animate.\n * The outline is measured once per resize and drawn as one path, so the hairline runs unbroken round the notch.\n * Under reduced motion the image switches to colour without a fade.\n */\nexport function NotchCard({\n  image,\n  title,\n  eyebrow,\n  children,\n  footer,\n  href,\n  linkProps,\n  action,\n  notch = 'top-right',\n  bloom,\n  className,\n  style,\n  ...rest\n}: NotchCardProps) {\n  const root = useRef<HTMLDivElement>(null)\n  const frame = useRef<SVGSVGElement>(null)\n  const left = notch === 'top-left'\n\n  // The clip and the hairline share one path, sized to the card. Nothing about it animates.\n  useIsoLayoutEffect(() => {\n    const el = root.current\n    const svg = frame.current\n    if (!el || !svg) return\n    const layout = () => {\n      const W = el.offsetWidth\n      const H = el.offsetHeight\n      if (!W || !H) return\n      const d = outline(W, H, left)\n      el.style.setProperty('--nc-clip', `path('${d}')`)\n      svg.setAttribute('viewBox', `0 0 ${W} ${H}`)\n      svg.querySelectorAll('path').forEach((p) => p.setAttribute('d', d))\n    }\n    layout()\n    const ro = new ResizeObserver(layout)\n    ro.observe(el)\n    return () => ro.disconnect()\n  }, [left])\n\n  const arrow = (\n    <span aria-hidden className=\"grid size-11 place-items-center rounded-full bg-muted text-foreground\">\n      <ArrowUpRight\n        strokeWidth={2}\n        className={cn(\n          'size-4 transition-transform duration-200 ease-out-quint motion-reduce:transition-none',\n          'group-hover/nc:translate-x-0.5 group-hover/nc:-translate-y-0.5 group-has-focus-visible/nc:translate-x-0.5 group-has-focus-visible/nc:-translate-y-0.5',\n          'group-data-bloom/nc:translate-x-0.5 group-data-bloom/nc:-translate-y-0.5',\n        )}\n      />\n    </span>\n  )\n  const inNotch = action ?? (href ? arrow : null)\n\n  return (\n    <div\n      ref={root}\n      data-bloom={bloom ? '' : undefined}\n      style={style}\n      className={cn('group/nc relative isolate flex w-full max-w-[280px] flex-col text-card-foreground', className)}\n      {...rest}\n    >\n      <div className=\"flex flex-1 flex-col bg-card [clip-path:var(--nc-clip)]\">\n        <div className=\"relative aspect-[280/214] shrink-0 overflow-hidden bg-muted\">\n          <div\n            className={cn(\n              'absolute inset-0 scale-[1.001] [filter:grayscale(1)_contrast(0.92)_brightness(1.02)] transition-[filter,scale] duration-280 ease-out-quint motion-reduce:transition-none',\n              '[&>img]:size-full [&>img]:object-cover [&>svg]:size-full',\n              'group-hover/nc:scale-[1.03] group-hover/nc:[filter:none] group-has-focus-visible/nc:scale-[1.03] group-has-focus-visible/nc:[filter:none]',\n              'group-data-bloom/nc:scale-[1.03] group-data-bloom/nc:[filter:none]',\n            )}\n          >\n            {image}\n          </div>\n        </div>\n        <div className=\"grid gap-2 px-6 pt-5 pb-4\">\n          {eyebrow && <p className=\"font-mono text-[11px] leading-none font-medium tracking-[0.04em] text-muted-foreground tabular-nums\">{eyebrow}</p>}\n          <h3 className=\"text-[19px] leading-[1.2] font-semibold tracking-[-0.02em] text-pretty\">\n            {href ? (\n              <a\n                href={href}\n                {...linkProps}\n                className={cn(\n                  'outline-none after:absolute after:inset-0 after:z-10 after:[clip-path:var(--nc-clip)] focus-visible:outline-none',\n                  linkProps?.className,\n                )}\n              >\n                {title}\n              </a>\n            ) : (\n              title\n            )}\n          </h3>\n          {children && <div className=\"text-[13.5px] leading-normal text-pretty text-muted-foreground\">{children}</div>}\n        </div>\n        {footer && (\n          <div className=\"mx-6 mt-auto mb-[18px] flex justify-between gap-3 border-t border-[color:color-mix(in_oklab,var(--foreground)_9%,transparent)] pt-3.5 font-mono text-[11.5px] leading-none text-muted-foreground tabular-nums\">\n            {footer}\n          </div>\n        )}\n      </div>\n      <svg ref={frame} aria-hidden className=\"pointer-events-none absolute inset-0 z-20 size-full overflow-visible\">\n        <path className=\"fill-none stroke-[color:color-mix(in_oklab,var(--foreground)_9%,transparent)] [stroke-width:1] [vector-effect:non-scaling-stroke]\" />\n        <path className=\"fill-none stroke-ring opacity-0 [stroke-width:2] [vector-effect:non-scaling-stroke] group-has-focus-visible/nc:opacity-100\" />\n      </svg>\n      {inNotch && (\n        <div className={cn('absolute top-0 z-30 grid size-14 place-items-center', left ? 'left-0' : 'right-0', !action && 'pointer-events-none')}>\n          {inNotch}\n        </div>\n      )}\n    </div>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "categories": [
    "surfaces"
  ],
  "type": "registry:component"
}