{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "feather-image", "title": "Feather Image", "description": "An image with no edge — the frame is dissolved with a mask so the photo bleeds into whatever ground it sits on. Bleed, bloom and veil masks, with optional blur and grain.", "dependencies": [ "class-variance-authority" ], "registryDependencies": [ "http://localhost:5173/r/liquid-theme.json" ], "files": [ { "path": "src/components/ui/feather-image.tsx", "content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\n/**\n * An image with no edge.\n *\n * Every other way of placing a photo on a page draws a boundary — a corner\n * radius, a border, a shadow. This one dissolves the photo into whatever it\n * sits on, so the page reads as one continuous surface with denser and\n * thinner regions rather than as a grid of cards. It is the move that makes\n * the reference layout feel like weather instead of furniture.\n *\n * Done with a mask rather than a gradient overlay on purpose: an overlay has\n * to know the page colour to fake the fade, and would go wrong the moment the\n * ground changed or the theme flipped. A mask takes the pixels away, so the\n * real ground shows through whatever it happens to be.\n */\nconst media = cva(\"relative block\", {\n variants: {\n variant: {\n /** Feathered on all four sides — the loose rectangle of the reference tiles. */\n bleed: \"\",\n /** Radial oval falloff — the blobby hero image with no straight edges anywhere. */\n bloom: \"\",\n /**\n * Solid at the top, dissolving downward. For images text has to sit on.\n * The only variant that keeps hard side edges, so it is also the only\n * one that has to clip its own blur compensation.\n */\n veil: \"overflow-hidden\",\n },\n },\n defaultVariants: { variant: \"bleed\" },\n})\n\n/**\n * Two crossed linear gradients composited with `intersect` give a rectangle\n * feathered on all four sides. A single radial gradient can't — it always\n * curves the corners in, which is right for `bloom` and wrong for `bleed`.\n */\nfunction maskFor(variant: string, feather: number): React.CSSProperties {\n const f = Math.round(Math.min(0.49, Math.max(0, feather)) * 100)\n\n if (variant === \"bloom\") {\n // `closest-side` is load-bearing: it pins the ellipse's radii to half the\n // box, so the gradient is guaranteed to have reached transparent by the\n // time it meets any edge. Sizing it in percentages instead lets the\n // ellipse run past the box, and the mask is still opaque where it gets\n // cut off — which shows up as exactly the hard edge the variant exists to\n // avoid.\n const core = Math.max(0, 100 - f * 2.4)\n const g = `radial-gradient(ellipse closest-side at 50% 50%, #000 ${core}%, transparent 100%)`\n return { maskImage: g, WebkitMaskImage: g }\n }\n\n if (variant === \"veil\") {\n return {\n maskImage: `linear-gradient(to bottom, #000 ${Math.max(0, 100 - f * 2.2)}%, transparent 100%)`,\n WebkitMaskImage: `linear-gradient(to bottom, #000 ${Math.max(0, 100 - f * 2.2)}%, transparent 100%)`,\n }\n }\n\n const horizontal = `linear-gradient(to right, transparent 0%, #000 ${f}%, #000 ${100 - f}%, transparent 100%)`\n const vertical = `linear-gradient(to bottom, transparent 0%, #000 ${f}%, #000 ${100 - f}%, transparent 100%)`\n return {\n maskImage: `${horizontal}, ${vertical}`,\n WebkitMaskImage: `${horizontal}, ${vertical}`,\n maskComposite: \"intersect\",\n WebkitMaskComposite: \"source-in\",\n }\n}\n\nexport interface FeatherImageProps\n extends Omit, \"children\">,\n VariantProps {\n src: string\n alt?: string\n /** Small caption laid over the image, as the reference tiles carry. */\n label?: React.ReactNode\n /** Where the caption sits. */\n labelPosition?: \"center\" | \"bottom\" | \"top\"\n /**\n * How much of the box the fade eats, per edge, as a fraction. 0.3 is the\n * reference; past ~0.45 the two masks meet and the middle starts to thin.\n */\n feather?: number\n /** `w / h`. Omit to let the image set its own height. */\n ratio?: number\n /** Blurs the image itself — the reference tiles are soft, not sharp. */\n blur?: number\n /** Film grain over the top, matched to the rest of the system. */\n grain?: boolean\n /** Slot for anything that should ride above the image: a play button, a badge. */\n overlay?: React.ReactNode\n}\n\nexport function FeatherImage({\n src,\n alt = \"\",\n label,\n labelPosition = \"center\",\n variant = \"bleed\",\n feather = 0.3,\n ratio,\n blur = 0,\n grain = false,\n overlay,\n className,\n style,\n ...props\n}: FeatherImageProps) {\n const mask = maskFor(variant ?? \"bleed\", feather)\n\n return (\n \n \n\n {/* The grain has to carry the same mask. It is a full-bleed rectangle,\n so left unmasked it redraws the exact edge the image just dissolved. */}\n {grain ? : null}\n {overlay}\n\n {label ? (\n \n {label}\n \n ) : null}\n \n )\n}\n", "type": "registry:ui" } ], "type": "registry:ui" }