{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "collage-frame",
  "title": "Masked Collage Frame",
  "description": "Image cut-outs as an arch, oval, torn paper or museum frame, with a tilt and an overlay slot for layering a modern UI card on top of a classical picture.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/collage-frame/collage-frame.tsx",
      "content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport type CollageShape = \"arch\" | \"oval\" | \"torn\" | \"museum\" | \"plain\"\n\nexport interface CollageFrameProps extends Omit<React.ComponentProps<\"div\">, \"children\"> {\n  /** Image URL. */\n  src: string\n  /** Accessible description. Leave empty for decorative images. */\n  alt?: string\n  /** Cut-out shape. Default \"arch\". */\n  shape?: CollageShape\n  /** Rotation in degrees, for a pasted-down collage feel. Default 0. */\n  tilt?: number\n  /** Soft drop shadow under the cut-out. Default true. */\n  shadow?: boolean\n  /** Torn edge roughness, 0 to 1. Default 0.5. */\n  roughness?: number\n  /** Seed for the torn edge, so two frames can differ. Default 7. */\n  seed?: number\n  /** Paper colour that shows along a torn edge. Default \"#f4efe6\". */\n  paperColor?: string\n  /** Museum frame colour. Default \"#2a2420\" (dark walnut). Try \"#b08d4a\" for gilt. */\n  frameColor?: string\n  /** Museum mat colour. Default \"#f3efe7\". */\n  matColor?: string\n  /** Museum frame thickness in px. Default 18. */\n  frameWidth?: number\n  /** Museum mat thickness in px. Default 24. */\n  matWidth?: number\n  /** How the image fills the cut-out. Default \"cover\". */\n  fit?: \"cover\" | \"contain\"\n  /** Class names for the img element. */\n  imageClassName?: string\n  /** Overlay content, usually one or more CollageFrameOverlay elements. */\n  children?: React.ReactNode\n}\n\n/** SVG mask with a displaced (torn) edge. `inset` is in viewBox units out of 1000. */\nfunction tornMask(seed: number, roughness: number, inset: number) {\n  const r = Math.min(1, Math.max(0, roughness))\n  const scale = 20 + r * 70\n  const bf = (0.006 + r * 0.016).toFixed(4)\n  const svg =\n    `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1000 1000' preserveAspectRatio='none'>` +\n    `<filter id='t' x='0' y='0' width='1000' height='1000' filterUnits='userSpaceOnUse'>` +\n    `<feTurbulence type='fractalNoise' baseFrequency='${bf}' numOctaves='4' seed='${seed}'/>` +\n    `<feDisplacementMap in='SourceGraphic' scale='${scale}' xChannelSelector='R' yChannelSelector='G'/>` +\n    `</filter>` +\n    `<rect x='${inset}' y='${inset}' width='${1000 - inset * 2}' height='${1000 - inset * 2}' fill='#000' filter='url(#t)'/>` +\n    `</svg>`\n  return `url(\"data:image/svg+xml;utf8,${encodeURIComponent(svg)}\")`\n}\n\nfunction maskStyle(mask: string): React.CSSProperties {\n  return {\n    WebkitMaskImage: mask,\n    maskImage: mask,\n    WebkitMaskSize: \"100% 100%\",\n    maskSize: \"100% 100%\",\n    WebkitMaskRepeat: \"no-repeat\",\n    maskRepeat: \"no-repeat\",\n  }\n}\n\n/**\n * An image cut out as an arch, oval, torn paper or museum frame, with room to layer\n * UI on top. Size it with className (width plus a height or aspect ratio).\n */\nexport function CollageFrame({\n  src,\n  alt = \"\",\n  shape = \"arch\",\n  tilt = 0,\n  shadow = true,\n  roughness = 0.5,\n  seed = 7,\n  paperColor = \"#f4efe6\",\n  frameColor = \"#2a2420\",\n  matColor = \"#f3efe7\",\n  frameWidth = 18,\n  matWidth = 24,\n  fit = \"cover\",\n  imageClassName,\n  className,\n  style,\n  children,\n  ...props\n}: CollageFrameProps) {\n  const img = (\n    // eslint-disable-next-line @next/next/no-img-element\n    <img\n      src={src}\n      alt={alt}\n      draggable={false}\n      className={cn(\"block size-full\", fit === \"cover\" ? \"object-cover\" : \"object-contain\", imageClassName)}\n    />\n  )\n\n  const softShadow = shadow ? \"shadow-[0_24px_48px_-16px_rgba(0,0,0,0.45)]\" : undefined\n\n  let picture: React.ReactNode\n  switch (shape) {\n    case \"oval\":\n      picture = <div className={cn(\"size-full overflow-hidden rounded-[50%]\", softShadow)}>{img}</div>\n      break\n    case \"plain\":\n      picture = <div className={cn(\"size-full overflow-hidden\", softShadow)}>{img}</div>\n      break\n    case \"torn\": {\n      const edge = 40 + roughness * 40\n      picture = (\n        <div\n          className=\"size-full\"\n          style={shadow ? { filter: \"drop-shadow(0 18px 28px rgba(0,0,0,0.35))\" } : undefined}\n        >\n          <div className=\"relative size-full\">\n            <div\n              aria-hidden\n              className=\"absolute inset-0\"\n              style={{ backgroundColor: paperColor, ...maskStyle(tornMask(seed, roughness, edge)) }}\n            />\n            <div className=\"absolute inset-0\" style={maskStyle(tornMask(seed, roughness, edge + 9))}>\n              {img}\n            </div>\n          </div>\n        </div>\n      )\n      break\n    }\n    case \"museum\":\n      picture = (\n        <div\n          className={cn(\"size-full\", softShadow)}\n          style={{\n            padding: frameWidth,\n            background: `linear-gradient(145deg, color-mix(in srgb, ${frameColor} 78%, white) 0%, ${frameColor} 38%, ${frameColor} 62%, color-mix(in srgb, ${frameColor} 70%, black) 100%)`,\n            boxShadow: `inset 0 0 0 1px color-mix(in srgb, ${frameColor} 60%, white), inset 0 0 0 ${frameWidth - 2}px transparent, inset 0 0 0 ${frameWidth - 1}px rgba(0,0,0,0.35)`,\n          }}\n        >\n          <div\n            className=\"size-full\"\n            style={{\n              padding: matWidth,\n              backgroundColor: matColor,\n              boxShadow: \"inset 0 1px 3px rgba(0,0,0,0.25), inset 0 0 0 1px rgba(0,0,0,0.06)\",\n            }}\n          >\n            <div className=\"size-full overflow-hidden shadow-[inset_0_2px_8px_rgba(0,0,0,0.35)]\">{img}</div>\n          </div>\n        </div>\n      )\n      break\n    case \"arch\":\n    default:\n      picture = <div className={cn(\"size-full overflow-hidden rounded-t-full\", softShadow)}>{img}</div>\n  }\n\n  return (\n    <div\n      data-slot=\"collage-frame\"\n      data-shape={shape}\n      className={cn(\"relative\", className)}\n      style={{ rotate: tilt ? `${tilt}deg` : undefined, ...style }}\n      {...props}\n    >\n      {picture}\n      {children}\n    </div>\n  )\n}\n\nexport type CollageOverlayPosition =\n  | \"top-left\"\n  | \"top-right\"\n  | \"bottom-left\"\n  | \"bottom-right\"\n  | \"bottom\"\n  | \"center\"\n\nconst positions: Record<CollageOverlayPosition, string> = {\n  \"top-left\": \"top-0 left-0 -translate-x-[var(--hang)] -translate-y-[var(--hang)]\",\n  \"top-right\": \"top-0 right-0 translate-x-[var(--hang)] -translate-y-[var(--hang)]\",\n  \"bottom-left\": \"bottom-0 left-0 -translate-x-[var(--hang)] translate-y-[var(--hang)]\",\n  \"bottom-right\": \"right-0 bottom-0 translate-x-[var(--hang)] translate-y-[var(--hang)]\",\n  bottom: \"bottom-0 left-1/2 -translate-x-1/2 translate-y-[var(--hang)]\",\n  center: \"top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2\",\n}\n\nexport interface CollageFrameOverlayProps extends React.ComponentProps<\"div\"> {\n  /** Where the overlay sits on the frame. Default \"bottom-left\". */\n  position?: CollageOverlayPosition\n  /** How far the overlay hangs past the frame edge, in px. Default 24. Use 0 to keep it inside. */\n  hang?: number\n  /** Counter-rotation in degrees, e.g. the negative of the frame tilt to keep UI level. Default 0. */\n  tilt?: number\n}\n\n/** Positions a card or any UI element on top of a CollageFrame. */\nexport function CollageFrameOverlay({\n  position = \"bottom-left\",\n  hang = 24,\n  tilt = 0,\n  className,\n  style,\n  ...props\n}: CollageFrameOverlayProps) {\n  return (\n    <div\n      data-slot=\"collage-frame-overlay\"\n      className={cn(\"absolute z-10 max-w-[88%]\", positions[position], className)}\n      style={\n        {\n          \"--hang\": `${hang}px`,\n          rotate: tilt ? `${tilt}deg` : undefined,\n          ...style,\n        } as React.CSSProperties\n      }\n      {...props}\n    />\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "meta": {
    "addedAt": "2026-10-23"
  },
  "type": "registry:component"
}