{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "offercard", "type": "registry:component", "title": "OfferCard", "description": "Componente Concorde OfferCard — un solo archivo, OfferCard.tsx.", "dependencies": [ "react" ], "registryDependencies": [], "files": [ { "path": "components/OfferCard.tsx", "type": "registry:file", "target": "components/OfferCard.tsx", "content": "\"use client\";\r\n\r\n/**\r\n * OfferCard — Generado por Concorde\r\n * Fuente: https://voyager-ds.vercel.app/preview/components/button-primary\r\n * Generado: 2026-05-28\r\n * EDITAR LIBREMENTE después de generar\r\n *\r\n * Variantes: live (orange bar, pprice+price+like) ·\r\n * negotiable (teal bar, solo like)\r\n */\r\n\r\nimport { useState, useCallback, useId } from \"react\";\r\nimport type { JSX } from \"react\";\r\n\r\n// ─── Types ────────────────────────────────────────────────────────────────────\r\n\r\nexport type OfferCardVariant = \"live\" | \"negotiable\";\r\n\r\nexport interface OfferCardProps {\r\n /** Variante — determina barra inferior y fila de precio */\r\n variant: OfferCardVariant;\r\n /** Tamaño. \"short\" = card compacta 134×170 (solo imagen + título/año + barra,\r\n * con el precio y el like como overlays sobre la imagen). Default \"full\". */\r\n size?: \"full\" | \"short\";\r\n /** Nombre del vehículo */\r\n name: string;\r\n /** Año */\r\n year: string | number;\r\n /**\r\n * Precio formateado (\"US$ 9,999\").\r\n * Si no se pasa, la fila de precio no se renderiza.\r\n * En negotiable solo aparece el like.\r\n */\r\n price?: string;\r\n /** URL imagen */\r\n imageSrc?: string;\r\n /** Alt text de imagen */\r\n imageAlt?: string;\r\n /** Badge de estado (pill BadgeStatus) — renderizado en img-badge slot */\r\n badge?: JSX.Element;\r\n /** Estado like controlado */\r\n liked?: boolean;\r\n /** Callback al alternar like */\r\n onLikeChange?: (liked: boolean) => void;\r\n /** Click en la card */\r\n onClick?: () => void;\r\n /** Aria label */\r\n \"aria-label\"?: string;\r\n /** Si false: showcase estático, sin hover/press. Default true */\r\n interactive?: boolean;\r\n /** Sombra elevada de Figma (X0 Y0 · Blur16 · Spread4 · #000000 10%). Default false */\r\n elevated?: boolean;\r\n className?: string;\r\n}\r\n\r\n// ─── Self-contained CSS ───────────────────────────────────────────────────────\r\n\r\nconst STYLE_ID = \"concorde-offercard-styles\";\r\n\r\nconst OFFERCARD_STYLES = `\r\n@keyframes offercard-heart-pop {\r\n 0% { transform: scale(1); }\r\n 35% { transform: scale(1.4); }\r\n 65% { transform: scale(0.85); }\r\n 85% { transform: scale(1.1); }\r\n 100% { transform: scale(1); }\r\n}\r\n@keyframes offercard-pulse {\r\n 0%, 100% { opacity: 1; }\r\n 50% { opacity: 0.5; }\r\n}\r\n\r\n/* ── Card base — ancho y alto fijo, igual en ambas variantes ── */\r\n.pcard {\r\n width: 170px;\r\n height: 232px;\r\n box-sizing: border-box;\r\n background: #ffffff;\r\n border-radius: 8px;\r\n box-shadow:\r\n rgba(0, 0, 0, 0.07) 0px 0px 16px 4px;\r\n display: flex;\r\n flex-direction: column;\r\n flex-shrink: 0;\r\n position: relative;\r\n overflow: hidden;\r\n transition:\r\n transform 150ms cubic-bezier(0.3, 0, 0, 1),\r\n box-shadow 150ms cubic-bezier(0.3, 0, 0, 1);\r\n cursor: pointer;\r\n}\r\n\r\n.pcard:hover:not(.pcard--disabled):not(.pcard--skeleton) {\r\n transform: translateY(-3px);\r\n box-shadow: oklch(0.22 0.18 285 / 0.12) 0px 12px 20px;\r\n}\r\n\r\n/* ── Image ── */\r\n.pcard__img {\r\n width: 100%;\r\n height: 112px;\r\n background: oklch(0.93 0.006 220);\r\n flex-shrink: 0;\r\n overflow: hidden;\r\n border-radius: 8px 8px 0 0;\r\n position: relative;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n}\r\n\r\n.pcard__img img {\r\n width: 100%;\r\n height: 100%;\r\n object-fit: cover;\r\n display: block;\r\n}\r\n\r\n.pcard__img-badge {\r\n position: absolute;\r\n top: 8px;\r\n right: 8px;\r\n z-index: 2;\r\n}\r\n\r\n/* ── Body ── */\r\n.pcard__body {\r\n display: flex;\r\n flex-direction: column;\r\n padding: 12px;\r\n height: 112px;\r\n flex-shrink: 0;\r\n box-sizing: border-box;\r\n}\r\n\r\n.pcard__meta {\r\n flex: 1;\r\n display: flex;\r\n flex-direction: column;\r\n gap: 2px;\r\n}\r\n\r\n.pcard__name {\r\n font-family: var(--vmc-font-display, 'Plus Jakarta Sans', sans-serif);\r\n font-size: 16px;\r\n font-weight: 700;\r\n line-height: 20px;\r\n color: #4c1ebc;\r\n white-space: nowrap;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n margin: 0;\r\n}\r\n\r\n.pcard__year {\r\n font-family: var(--vmc-font-display, 'Plus Jakarta Sans', sans-serif);\r\n font-size: 11px;\r\n font-weight: 500;\r\n line-height: 16px;\r\n letter-spacing: 0.06em;\r\n text-transform: uppercase;\r\n color: #191c1c;\r\n margin: 0;\r\n}\r\n\r\n/* Price row — pprice + price text + like */\r\n.pcard__price-row {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n margin-top: 10px;\r\n flex-shrink: 0;\r\n}\r\n\r\n.pcard__price-left {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n}\r\n\r\n.pcard__price-text {\r\n font-family: var(--vmc-font-mono, 'Roboto Mono', monospace);\r\n font-size: 13px;\r\n font-weight: 700;\r\n line-height: 20px;\r\n font-variant-numeric: tabular-nums;\r\n color: #4c1ebc;\r\n white-space: nowrap;\r\n}\r\n\r\n/* Like row — solo like (negotiable) */\r\n.pcard__like-row {\r\n display: flex;\r\n align-items: center;\r\n justify-content: flex-end;\r\n margin-top: 10px;\r\n flex-shrink: 0;\r\n}\r\n\r\n/* ── Variants — bottom bars ── */\r\n.pcard--live {\r\n position: relative;\r\n}\r\n.pcard--live::after {\r\n content: \"\";\r\n position: absolute;\r\n bottom: 0;\r\n left: 0;\r\n right: 0;\r\n height: 8px;\r\n background: linear-gradient(90deg, #ff9639 0%, #ef852e 50%, #be3d00 100%);\r\n border-radius: 0 0 var(--vmc-radius-sm, 4px) var(--vmc-radius-sm, 4px);\r\n}\r\n\r\n.pcard--negotiable {\r\n position: relative;\r\n}\r\n.pcard--negotiable::after {\r\n content: \"\";\r\n position: absolute;\r\n bottom: 0;\r\n left: 0;\r\n right: 0;\r\n height: 8px;\r\n background: linear-gradient(90deg, #00edee 0%, #00d2d3 50%, #009597 100%);\r\n border-radius: 0 0 var(--vmc-radius-sm, 4px) var(--vmc-radius-sm, 4px);\r\n}\r\n\r\n\r\n/* ── Price gem (auction diamond, paths exactos de Figma) ── */\r\n.pcard-pprice {\r\n width: 30px;\r\n height: 32px;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n flex-shrink: 0;\r\n}\r\n\r\n/* ── Mini like button (self-contained) ── */\r\n.pcard-like {\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n width: 30px;\r\n height: 30px;\r\n border-radius: 9999px;\r\n border: 1.44px solid transparent;\r\n background-image:\r\n linear-gradient(#ffffff, #ffffff),\r\n linear-gradient(135deg, #e8ddff 0%, #ffffff 40%, #cfbaff 75%, #ae8eff 100%);\r\n background-origin: padding-box, border-box;\r\n background-clip: padding-box, border-box;\r\n cursor: pointer;\r\n padding: 2px;\r\n position: relative;\r\n overflow: hidden;\r\n box-shadow: rgba(132, 96, 229, 0.14) 0px 2px 8px;\r\n transition:\r\n transform 0.15s cubic-bezier(0.25, 0.8, 0.25, 1),\r\n box-shadow 0.2s;\r\n flex-shrink: 0;\r\n outline: none;\r\n}\r\n/* Glass highlight (white 0.55 → transparent) */\r\n.pcard-like::before {\r\n content: \"\";\r\n position: absolute;\r\n inset: 0;\r\n border-radius: 9999px;\r\n background: linear-gradient(rgba(255, 255, 255, 0.55) 0%, transparent 50%);\r\n pointer-events: none;\r\n z-index: 1;\r\n}\r\n.pcard-like svg { position: relative; z-index: 2; }\r\n.pcard-like:hover {\r\n transform: scale(1.1) translateY(-1px);\r\n box-shadow: rgba(32, 0, 104, 0.2) 0px 6px 14px;\r\n}\r\n.pcard-like:active { transform: scale(0.92); }\r\n.pcard-like--active {\r\n background-image:\r\n linear-gradient(135deg, #8460e5 0%, #3b1782 100%),\r\n linear-gradient(135deg, #fbc47d 0%, #fbc47d 100%);\r\n box-shadow: rgba(132, 96, 229, 0.35) 0px 2px 8px, inset 0px 1px 0px rgba(255,255,255,0.22);\r\n}\r\n.pcard-like--pop svg {\r\n animation: offercard-heart-pop 0.38s cubic-bezier(0.34, 1.56, 0.64, 1) 1;\r\n}\r\n.pcard-like:focus-visible {\r\n outline: 2px solid oklch(0.62 0.20 285);\r\n outline-offset: 2px;\r\n}\r\n\r\n/* ── Sombra elevada — token \"card\" de Figma (X0 Y0 · Blur16 · Spread4 · #000000 10%) ── */\r\n.pcard--elevated { box-shadow: rgba(0, 0, 0, 0.1) 0px 0px 16px 4px; }\r\n.pcard--static.pcard--elevated:hover { box-shadow: rgba(0, 0, 0, 0.1) 0px 0px 16px 4px; }\r\n\r\n/* ── Static showcase (sin hover/press) ── */\r\n.pcard--static, .pcard--static .pcard-like { transition: none; cursor: default; }\r\n.pcard--static:hover { transform: none; box-shadow: rgba(0, 0, 0, 0.07) 0px 0px 16px 4px; }\r\n.pcard--static .pcard-like { pointer-events: none; }\r\n.pcard--static .pcard-like:hover { transform: none; box-shadow: rgba(132, 96, 229, 0.14) 0px 2px 8px; }\r\n.pcard--static .pcard-like:active { transform: none; }\r\n\r\n/* ── Short variant (134×170) — imagen 134×112 + título/año 134×50 + barra 134×8.\r\n Precio (99×24) y like (24×24) van como overlays SOBRE la imagen. ── */\r\n.pcard--short { width: 134px; height: 170px; }\r\n.pcard--short .pcard__img { height: 112px; }\r\n.pcard__body--short {\r\n height: 50px;\r\n box-sizing: border-box;\r\n padding: 8px 10px;\r\n display: flex;\r\n flex-direction: column;\r\n justify-content: center;\r\n gap: 1px;\r\n}\r\n.pcard--short .pcard__name { font-size: 15px; line-height: 18px; }\r\n.pcard--short .pcard__year { font-size: 10px; line-height: 14px; }\r\n\r\n/* Precio como badge (99×24) — arriba a la izquierda de la imagen */\r\n.pcard__price-badge {\r\n position: absolute;\r\n top: 8px;\r\n left: 7px;\r\n height: 24px;\r\n box-sizing: border-box;\r\n padding: 0 10px 0 3px;\r\n border-radius: 12px;\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 3px;\r\n border: 1px solid transparent;\r\n background-image:\r\n linear-gradient(135deg, #8460e5 0%, #3b1782 100%),\r\n linear-gradient(135deg, #8776ff 0%, rgba(255,255,255,0.4) 38%, #532bc7 68%, #8776ff 100%);\r\n background-origin: padding-box, border-box;\r\n background-clip: padding-box, border-box;\r\n box-shadow: rgba(32, 0, 104, 0.5) 0px 2px 6px;\r\n z-index: 2;\r\n}\r\n.pcard__price-badge::before {\r\n content: \"\";\r\n position: absolute;\r\n inset: 0;\r\n border-radius: 12px;\r\n background: linear-gradient(180deg, rgba(255,255,255,0.1) 0%, transparent 100%);\r\n pointer-events: none;\r\n}\r\n.pcard__price-badge-gem { width: 20px; height: 22px; display: inline-flex; position: relative; z-index: 1; flex-shrink: 0; }\r\n.pcard__price-badge-text {\r\n position: relative;\r\n z-index: 1;\r\n font-family: var(--vmc-font-mono, 'Roboto Mono', monospace);\r\n font-size: 13px;\r\n font-weight: 700;\r\n color: #ffffff;\r\n white-space: nowrap;\r\n font-variant-numeric: tabular-nums;\r\n}\r\n\r\n/* Like (24×24) — abajo a la derecha de la imagen */\r\n.pcard__img-like { position: absolute; bottom: 8px; right: 9px; z-index: 2; }\r\n.pcard--short .pcard__img-like .pcard-like { width: 22px; height: 22px; }\r\n.pcard--short .pcard__img-like .pcard-like svg { width: 12px; height: 12px; }\r\n\r\n/* ── Reduced motion ── */\r\n@media (prefers-reduced-motion: reduce) {\r\n .pcard, .pcard-like { transition: none; }\r\n .pcard-like--pop svg { animation: none; }\r\n}\r\n`;\r\n\r\nconst HEART_PATH = \"M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z\";\r\n\r\nlet _stylesInjected = false;\r\n\r\n// ─── Which variants show price row vs like row ────────────────────────────────\r\n\r\nconst SHOW_PRICE: Record = {\r\n live: true, // pprice + price + like\r\n negotiable: false, // solo like\r\n};\r\n\r\n// ─── Component ────────────────────────────────────────────────────────────────\r\n\r\nexport default function OfferCard({\r\n variant,\r\n size = \"full\",\r\n name,\r\n year,\r\n price,\r\n imageSrc,\r\n imageAlt,\r\n badge,\r\n liked: controlledLiked,\r\n onLikeChange,\r\n onClick,\r\n \"aria-label\": ariaLabel,\r\n interactive = true,\r\n elevated = false,\r\n className = \"\",\r\n}: OfferCardProps): JSX.Element {\r\n if (typeof document !== \"undefined\" && !_stylesInjected) {\r\n if (!document.getElementById(STYLE_ID)) {\r\n const el = document.createElement(\"style\");\r\n el.id = STYLE_ID;\r\n el.textContent = OFFERCARD_STYLES;\r\n document.head.appendChild(el);\r\n }\r\n _stylesInjected = true;\r\n }\r\n\r\n const uid = useId().replace(/:/g, \"-\");\r\n\r\n const isControlled = controlledLiked !== undefined;\r\n const [internalLiked, setInternalLiked] = useState(false);\r\n const isLiked = isControlled ? controlledLiked : internalLiked;\r\n const [popping, setPopping] = useState(false);\r\n\r\n const handleLike = useCallback((e: React.MouseEvent) => {\r\n e.stopPropagation();\r\n const next = !isLiked;\r\n if (!isControlled) setInternalLiked(next);\r\n onLikeChange?.(next);\r\n if (next) {\r\n setPopping(true);\r\n setTimeout(() => { setPopping(false); }, 420);\r\n }\r\n }, [isLiked, isControlled, onLikeChange]);\r\n\r\n const showPriceRow = SHOW_PRICE[variant] && (price != null);\r\n const showLikeRow = !showPriceRow;\r\n\r\n const cardClasses = [\r\n \"pcard\",\r\n `pcard--${variant}`,\r\n size === \"short\" ? \"pcard--short\" : \"\",\r\n interactive ? \"\" : \"pcard--static\",\r\n elevated ? \"pcard--elevated\" : \"\",\r\n className,\r\n ].filter(Boolean).join(\" \");\r\n\r\n const likeClasses = [\r\n \"pcard-like\",\r\n isLiked ? \"pcard-like--active\" : \"\",\r\n popping ? \"pcard-like--pop\" : \"\",\r\n ].filter(Boolean).join(\" \");\r\n\r\n const svgFill = isLiked ? \"rgba(255,255,255,0.92)\" : \"#ffffff\";\r\n const svgStroke = isLiked ? \"none\" : \"#5a35c2\";\r\n\r\n const LikeBtn = (\r\n \r\n \r\n \r\n \r\n \r\n );\r\n\r\n // Gema del precio (diamante de subasta) — reutilizada en la fila (full) y en el badge (short)\r\n const PriceGem = ({ w, h }: { w: number; h: number }): JSX.Element => (\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n );\r\n\r\n // ── Short variant: imagen con precio + like overlay, luego título/año, barra por ::after ──\r\n if (size === \"short\") {\r\n return (\r\n <>\r\n