{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "instagram-post",
  "type": "registry:block",
  "title": "Instagram Post",
  "description": "Render Instagram post previews.",
  "dependencies": [
    "lucide-react",
    "zod"
  ],
  "registryDependencies": [
    "button",
    "tooltip"
  ],
  "files": [
    {
      "path": "components/tool-ui/instagram-post/_adapter.tsx",
      "type": "registry:component",
      "target": "components/tool-ui/instagram-post/_adapter.tsx",
      "content": "/**\n * Adapter: UI and utility re-exports for copy-standalone portability.\n *\n * When copying this component to another project, update these imports\n * to match your project's paths:\n *\n *   cn      → Your Tailwind merge utility (e.g., \"@/lib/utils\", \"~/lib/cn\")\n *   Button  → shadcn/ui Button\n *   Tooltip → shadcn/ui Tooltip\n */\n\nexport { cn } from \"@/lib/utils\";\nexport { Button } from \"@/components/ui/button\";\nexport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\n"
    },
    {
      "path": "components/tool-ui/instagram-post/index.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/instagram-post/index.ts",
      "content": "export { InstagramPost } from \"./instagram-post\";\nexport type { InstagramPostProps } from \"./instagram-post\";\nexport type {\n  InstagramPostData,\n  InstagramPostAuthor,\n  InstagramPostMedia,\n  InstagramPostStats,\n} from \"./schema\";\n"
    },
    {
      "path": "components/tool-ui/instagram-post/instagram-post.tsx",
      "type": "registry:component",
      "target": "components/tool-ui/instagram-post/instagram-post.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { BadgeCheck, Heart, Share } from \"lucide-react\";\nimport {\n  cn,\n  Button,\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"./_adapter\";\nimport { formatRelativeTime } from \"../shared/utils\";\n\nimport type { InstagramPostData, InstagramPostMedia } from \"./schema\";\n\nexport interface InstagramPostProps {\n  post: InstagramPostData;\n  className?: string;\n  onAction?: (action: string, post: InstagramPostData) => void;\n}\n\nfunction InstagramLogo({ className }: { className?: string }) {\n  const id = React.useId();\n  const gradientPrimaryId = `ig-primary-${id}`;\n  const gradientSecondaryId = `ig-secondary-${id}`;\n\n  return (\n    <svg\n      viewBox=\"0 0 132 132\"\n      className={className}\n      role=\"img\"\n      aria-label=\"Instagram logo\"\n    >\n      <defs>\n        <radialGradient\n          id={gradientPrimaryId}\n          cx=\"158.429\"\n          cy=\"578.088\"\n          r=\"65\"\n          gradientUnits=\"userSpaceOnUse\"\n          gradientTransform=\"matrix(0 -1.982 1.844 0 -1031.4 454)\"\n        >\n          <stop offset=\"0\" stopColor=\"#fd5\" />\n          <stop offset=\".1\" stopColor=\"#fd5\" />\n          <stop offset=\".5\" stopColor=\"#ff543e\" />\n          <stop offset=\"1\" stopColor=\"#c837ab\" />\n        </radialGradient>\n        <radialGradient\n          id={gradientSecondaryId}\n          cx=\"147.694\"\n          cy=\"473.455\"\n          r=\"65\"\n          gradientUnits=\"userSpaceOnUse\"\n          gradientTransform=\"matrix(.174 .869 -3.58 .717 1648 -458.5)\"\n        >\n          <stop offset=\"0\" stopColor=\"#3771c8\" />\n          <stop offset=\".128\" stopColor=\"#3771c8\" />\n          <stop offset=\"1\" stopColor=\"#60f\" stopOpacity=\"0\" />\n        </radialGradient>\n      </defs>\n      <path\n        fill={`url(#${gradientPrimaryId})`}\n        d=\"M65 0C37.9 0 30 .03 28.4.16c-5.6.46-9 1.34-12.8 3.22-2.9 1.44-5.2 3.12-7.5 5.47C4 13.1 1.5 18.4.6 24.66c-.44 3.04-.57 3.66-.6 19.2-.01 5.16 0 12 0 21.1 0 27.12.03 35.05.16 36.6.45 5.4 1.3 8.82 3.1 12.55 3.44 7.14 10 12.5 17.76 14.5 2.68.7 5.64 1.1 9.44 1.26 1.6.07 18 .12 34.44.12s32.84-.02 34.4-.1c4.4-.2 6.96-.55 9.8-1.28 7.78-2.01 14.23-7.3 17.74-14.53 1.76-3.64 2.66-7.18 3.07-12.32.08-1.12.12-18.97.12-36.8 0-17.85-.04-35.67-.13-36.8-.4-5.2-1.3-8.7-3.13-12.43-1.5-3.04-3.16-5.3-5.56-7.62C116.9 4 111.64 1.5 105.37.6 102.34.16 101.73.03 86.2 0H65z\"\n        transform=\"translate(1 1)\"\n      />\n      <path\n        fill={`url(#${gradientSecondaryId})`}\n        d=\"M65 0C37.9 0 30 .03 28.4.16c-5.6.46-9 1.34-12.8 3.22-2.9 1.44-5.2 3.12-7.5 5.47C4 13.1 1.5 18.4.6 24.66c-.44 3.04-.57 3.66-.6 19.2-.01 5.16 0 12 0 21.1 0 27.12.03 35.05.16 36.6.45 5.4 1.3 8.82 3.1 12.55 3.44 7.14 10 12.5 17.76 14.5 2.68.7 5.64 1.1 9.44 1.26 1.6.07 18 .12 34.44.12s32.84-.02 34.4-.1c4.4-.2 6.96-.55 9.8-1.28 7.78-2.01 14.23-7.3 17.74-14.53 1.76-3.64 2.66-7.18 3.07-12.32.08-1.12.12-18.97.12-36.8 0-17.85-.04-35.67-.13-36.8-.4-5.2-1.3-8.7-3.13-12.43-1.5-3.04-3.16-5.3-5.56-7.62C116.9 4 111.64 1.5 105.37.6 102.34.16 101.73.03 86.2 0H65z\"\n        transform=\"translate(1 1)\"\n      />\n      <path\n        fill=\"#fff\"\n        d=\"M66 18c-13 0-14.67.06-19.8.3-5.1.23-8.6 1.04-11.64 2.22-3.16 1.23-5.84 2.87-8.5 5.54-2.67 2.67-4.3 5.35-5.54 8.5-1.2 3.05-2 6.54-2.23 11.65C18.06 51.33 18 52.96 18 66s.06 14.67.3 19.78c.22 5.12 1.03 8.6 2.22 11.66 1.22 3.15 2.86 5.83 5.53 8.5 2.67 2.67 5.35 4.3 8.5 5.53 3.06 1.2 6.55 2 11.65 2.23 5.12.23 6.76.3 19.8.3 13 0 14.66-.07 19.78-.3 5.12-.23 8.6-1.03 11.66-2.23 3.15-1.23 5.83-2.87 8.5-5.53 2.67-2.67 4.3-5.35 5.53-8.5 1.2-3.06 2-6.54 2.23-11.66.23-5.1.3-6.75.3-19.78 0-13.04-.07-14.68-.3-19.8-.23-5.1-1.04-8.6-2.22-11.64-1.23-3.16-2.87-5.84-5.54-8.5-2.67-2.67-5.35-4.3-8.5-5.54-3.06-1.18-6.55-2-11.66-2.22-5.12-.24-6.75-.3-19.8-.3zm-4.3 8.65c1.28 0 2.7 0 4.3 0 12.82 0 14.34.05 19.4.28 4.67.2 7.22 1 8.9 1.65 2.25.87 3.84 1.9 5.52 3.6 1.68 1.67 2.72 3.27 3.6 5.5.65 1.7 1.43 4.24 1.64 8.92.23 5.05.28 6.57.28 19.4s-.05 14.32-.28 19.4c-.2 4.67-1 7.2-1.64 8.9-.88 2.25-1.92 3.84-3.6 5.52-1.68 1.68-3.27 2.72-5.52 3.6-1.7.65-4.23 1.43-8.9 1.64-5.06.23-6.58.28-19.4.28-12.82 0-14.34-.05-19.4-.28-4.68-.2-7.22-1-8.9-1.64-2.25-.88-3.84-1.92-5.52-3.6-1.68-1.68-2.72-3.27-3.6-5.52-.65-1.7-1.43-4.23-1.64-8.9-.23-5.06-.28-6.58-.28-19.4s.05-14.34.28-19.4c.2-4.68 1-7.22 1.64-8.9.88-2.24 1.92-3.83 3.6-5.52 1.68-1.68 3.27-2.72 5.52-3.6 1.7-.65 4.23-1.43 8.9-1.65 4.43-.2 6.15-.26 15.1-.27zm30 8c-3.2 0-5.77 2.57-5.77 5.75 0 3.2 2.58 5.77 5.77 5.77 3.18 0 5.76-2.58 5.76-5.77 0-3.18-2.58-5.76-5.76-5.76zm-25.63 6.72c-13.6 0-24.64 11.04-24.64 24.65 0 13.6 11.03 24.64 24.64 24.64 13.6 0 24.65-11.03 24.65-24.64 0-13.6-11.04-24.64-24.65-24.64zm0 8.65c8.84 0 16 7.16 16 16 0 8.84-7.16 16-16 16-8.84 0-16-7.16-16-16 0-8.84 7.16-16 16-16z\"\n      />\n    </svg>\n  );\n}\n\nfunction Header({\n  author,\n  createdAt,\n}: {\n  author: InstagramPostData[\"author\"];\n  createdAt?: string;\n}) {\n  return (\n    <header className=\"flex items-center gap-3 p-3\">\n      <img\n        src={author.avatarUrl}\n        alt={`${author.name} avatar`}\n        width={32}\n        height={32}\n        className=\"size-8 rounded-full object-cover\"\n      />\n      <div className=\"flex min-w-0 flex-1 items-center gap-1.5\">\n        <span className=\"truncate text-sm font-semibold\">{author.handle}</span>\n        {author.verified && (\n          <BadgeCheck\n            aria-label=\"Verified\"\n            className=\"size-3.5 shrink-0 text-sky-500\"\n          />\n        )}\n        {createdAt && (\n          <>\n            <span className=\"text-muted-foreground\">·</span>\n            <span className=\"text-muted-foreground text-sm\">\n              {formatRelativeTime(createdAt)}\n            </span>\n          </>\n        )}\n      </div>\n      <InstagramLogo className=\"size-5\" />\n    </header>\n  );\n}\n\nfunction MediaGrid({\n  media,\n  onOpen,\n}: {\n  media: InstagramPostMedia[];\n  onOpen?: (index: number) => void;\n}) {\n  if (media.length === 0) return null;\n\n  const renderItem = (item: InstagramPostMedia, index: number) => (\n    <button\n      key={index}\n      type=\"button\"\n      className=\"bg-muted relative block size-full overflow-hidden\"\n      onClick={() => onOpen?.(index)}\n    >\n      {item.type === \"image\" ? (\n        <img\n          src={item.url}\n          alt={item.alt}\n          className=\"size-full object-cover\"\n          loading=\"lazy\"\n        />\n      ) : (\n        <video src={item.url} playsInline className=\"size-full object-cover\" />\n      )}\n    </button>\n  );\n\n  if (media.length === 1) {\n    return (\n      <div className=\"aspect-square w-full overflow-hidden\">\n        {renderItem(media[0], 0)}\n      </div>\n    );\n  }\n\n  if (media.length === 2) {\n    return (\n      <div className=\"grid aspect-square w-full grid-cols-2 gap-0.5 overflow-hidden\">\n        {media.map(renderItem)}\n      </div>\n    );\n  }\n\n  if (media.length === 3) {\n    return (\n      <div className=\"grid aspect-square w-full grid-cols-2 gap-0.5 overflow-hidden\">\n        <div className=\"h-full\">{renderItem(media[0], 0)}</div>\n        <div className=\"grid h-full grid-rows-2 gap-0.5\">\n          {media.slice(1).map((item, i) => (\n            <div key={i + 1} className=\"h-full\">\n              {renderItem(item, i + 1)}\n            </div>\n          ))}\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"grid aspect-square w-full grid-cols-2 gap-0.5 overflow-hidden\">\n      {media.slice(0, 4).map((item, index) => (\n        <div key={index} className=\"relative h-full w-full\">\n          {renderItem(item, index)}\n          {index === 3 && media.length > 4 && (\n            <div className=\"pointer-events-none absolute inset-0 flex items-center justify-center bg-black/50\">\n              <span className=\"text-2xl font-semibold text-white\">\n                +{media.length - 4}\n              </span>\n            </div>\n          )}\n        </div>\n      ))}\n    </div>\n  );\n}\n\nfunction PostBody({ text }: { text?: string }) {\n  if (!text) return null;\n  return (\n    <span className=\"text-sm leading-relaxed text-pretty wrap-break-word whitespace-pre-wrap\">\n      {text}\n    </span>\n  );\n}\n\nfunction ActionButton({\n  icon: Icon,\n  label,\n  active,\n  hoverColor,\n  activeColor,\n  onClick,\n}: {\n  icon: React.ComponentType<{ className?: string }>;\n  label: string;\n  active?: boolean;\n  hoverColor: string;\n  activeColor?: string;\n  onClick: () => void;\n}) {\n  return (\n    <Tooltip>\n      <TooltipTrigger asChild>\n        <Button\n          variant=\"ghost\"\n          size=\"sm\"\n          onClick={(e) => {\n            e.stopPropagation();\n            onClick();\n          }}\n          className={cn(\"h-auto\", hoverColor, active && activeColor)}\n          aria-label={label}\n        >\n          <Icon className=\"size-5\" />\n        </Button>\n      </TooltipTrigger>\n      <TooltipContent>{label}</TooltipContent>\n    </Tooltip>\n  );\n}\n\nfunction PostActions({\n  stats,\n  onAction,\n}: {\n  stats?: InstagramPostData[\"stats\"];\n  onAction: (action: string) => void;\n}) {\n  return (\n    <TooltipProvider delayDuration={300}>\n      <div className=\"flex items-center gap-1\">\n        <ActionButton\n          icon={Heart}\n          label=\"Like\"\n          active={stats?.isLiked}\n          hoverColor=\"hover:opacity-60\"\n          activeColor=\"text-red-500 fill-red-500\"\n          onClick={() => onAction(\"like\")}\n        />\n        <ActionButton\n          icon={Share}\n          label=\"Share\"\n          hoverColor=\"hover:opacity-60\"\n          onClick={() => onAction(\"share\")}\n        />\n      </div>\n    </TooltipProvider>\n  );\n}\n\nexport function InstagramPost({\n  post,\n  className,\n  onAction,\n}: InstagramPostProps) {\n  return (\n    <div\n      className={cn(\"flex max-w-xl flex-col gap-3\", className)}\n      data-tool-ui-id={post.id}\n      data-slot=\"instagram-post\"\n    >\n      <article className=\"bg-card overflow-hidden rounded-lg border shadow-sm\">\n        <Header author={post.author} createdAt={post.createdAt} />\n\n        {post.media && post.media.length > 0 && (\n          <MediaGrid media={post.media} />\n        )}\n\n        <div className=\"flex flex-col gap-2 p-3\">\n          <PostActions\n            stats={post.stats}\n            onAction={(action) => onAction?.(action, post)}\n          />\n          {post.text && (\n            <div>\n              <span className=\"text-sm font-semibold\">\n                {post.author.handle}\n              </span>{\" \"}\n              <PostBody text={post.text} />\n            </div>\n          )}\n        </div>\n      </article>\n    </div>\n  );\n}\n"
    },
    {
      "path": "components/tool-ui/instagram-post/README.md",
      "type": "registry:file",
      "target": "components/tool-ui/instagram-post/README.md",
      "content": "# Instagram Post\n\nImplementation for the \"instagram-post\" Tool UI surface.\n\n## Files\n\n- public exports: components/tool-ui/instagram-post/index.ts\n- serializable schema + parse helpers: components/tool-ui/instagram-post/schema.ts\n\n## Companion assets\n\n- Docs page: app/docs/social-post/content.mdx\n- Preset payload: lib/presets/instagram-post.ts\n\n## Quick check\n\nRun this after edits:\n\npnpm test\n"
    },
    {
      "path": "components/tool-ui/instagram-post/schema.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/instagram-post/schema.ts",
      "content": "import { z } from \"zod\";\nimport { defineToolUiContract } from \"../shared/contract\";\n\nexport const InstagramPostAuthorSchema = z.object({\n  name: z.string(),\n  handle: z.string(),\n  avatarUrl: z.string(),\n  verified: z.boolean().optional(),\n});\n\nexport const InstagramPostMediaSchema = z.object({\n  type: z.enum([\"image\", \"video\"]),\n  url: z.string(),\n  alt: z.string(),\n});\n\nexport const InstagramPostStatsSchema = z.object({\n  likes: z.number().optional(),\n  isLiked: z.boolean().optional(),\n});\n\nexport interface InstagramPostData {\n  id: string;\n  author: z.infer<typeof InstagramPostAuthorSchema>;\n  text?: string;\n  media?: z.infer<typeof InstagramPostMediaSchema>[];\n  stats?: z.infer<typeof InstagramPostStatsSchema>;\n  createdAt?: string;\n}\n\nexport const SerializableInstagramPostSchema: z.ZodType<InstagramPostData> =\n  z.object({\n    id: z.string(),\n    author: InstagramPostAuthorSchema,\n    text: z.string().optional(),\n    media: z.array(InstagramPostMediaSchema).optional(),\n    stats: InstagramPostStatsSchema.optional(),\n    createdAt: z.string().optional(),\n  });\n\nexport type InstagramPostAuthor = z.infer<typeof InstagramPostAuthorSchema>;\nexport type InstagramPostMedia = z.infer<typeof InstagramPostMediaSchema>;\nexport type InstagramPostStats = z.infer<typeof InstagramPostStatsSchema>;\n\nconst SerializableInstagramPostSchemaContract = defineToolUiContract(\n  \"InstagramPost\",\n  SerializableInstagramPostSchema,\n);\n\nexport const parseSerializableInstagramPost: (\n  input: unknown,\n) => InstagramPostData = SerializableInstagramPostSchemaContract.parse;\n\nexport const safeParseSerializableInstagramPost: (\n  input: unknown,\n) => InstagramPostData | null =\n  SerializableInstagramPostSchemaContract.safeParse;\n"
    },
    {
      "path": "components/tool-ui/shared/contract.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/shared/contract.ts",
      "content": "import { z } from \"zod\";\nimport { parseWithSchema, safeParseWithSchema } from \"./parse\";\n\nexport interface ToolUiContract<T> {\n  schema: z.ZodType<T>;\n  parse: (input: unknown) => T;\n  safeParse: (input: unknown) => T | null;\n}\n\nexport function defineToolUiContract<T>(\n  componentName: string,\n  schema: z.ZodType<T>,\n): ToolUiContract<T> {\n  return {\n    schema,\n    parse: (input: unknown) => parseWithSchema(schema, input, componentName),\n    safeParse: (input: unknown) => safeParseWithSchema(schema, input),\n  };\n}\n"
    },
    {
      "path": "components/tool-ui/shared/parse.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/shared/parse.ts",
      "content": "import { z } from \"zod\";\n\nfunction formatZodPath(path: Array<string | number | symbol>): string {\n  if (path.length === 0) return \"root\";\n  return path\n    .map((segment) =>\n      typeof segment === \"number\" ? `[${segment}]` : String(segment),\n    )\n    .join(\".\");\n}\n\n/**\n * Format Zod errors into a compact `path: message` string.\n */\nexport function formatZodError(error: z.ZodError): string {\n  const parts = error.issues.map((issue) => {\n    const path = formatZodPath(issue.path);\n    return `${path}: ${issue.message}`;\n  });\n\n  return Array.from(new Set(parts)).join(\"; \");\n}\n\n/**\n * Parse unknown input and throw a readable error.\n */\nexport function parseWithSchema<T>(\n  schema: z.ZodType<T>,\n  input: unknown,\n  name: string,\n): T {\n  const res = schema.safeParse(input);\n  if (!res.success) {\n    throw new Error(`Invalid ${name} payload: ${formatZodError(res.error)}`);\n  }\n  return res.data;\n}\n\n/**\n * Parse unknown input, returning `null` instead of throwing on failure.\n *\n * Use this in assistant-ui `render` functions where `args` stream in\n * incrementally and may be incomplete until the tool call finishes.\n */\nexport function safeParseWithSchema<T>(\n  schema: z.ZodType<T>,\n  input: unknown,\n): T | null {\n  const res = schema.safeParse(input);\n  return res.success ? res.data : null;\n}\n"
    },
    {
      "path": "components/tool-ui/shared/utils.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/shared/utils.ts",
      "content": "export function formatRelativeTime(iso: string): string {\n  const seconds = Math.round((Date.now() - new Date(iso).getTime()) / 1000);\n  if (seconds < 60) return `${seconds}s`;\n  if (seconds < 3600) return `${Math.round(seconds / 60)}m`;\n  if (seconds < 86400) return `${Math.round(seconds / 3600)}h`;\n  if (seconds < 604800) return `${Math.round(seconds / 86400)}d`;\n  return `${Math.round(seconds / 604800)}w`;\n}\n\nexport function formatCount(count: number): string {\n  if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;\n  if (count >= 1_000) return `${(count / 1_000).toFixed(1)}K`;\n  return String(count);\n}\n\nexport function getDomain(url: string): string {\n  try {\n    return new URL(url).hostname.replace(/^www\\./, \"\");\n  } catch {\n    return \"\";\n  }\n}\n\nexport function prefersReducedMotion(): boolean {\n  return (\n    typeof window !== \"undefined\" &&\n    window.matchMedia?.(\"(prefers-reduced-motion: reduce)\").matches\n  );\n}\n"
    }
  ]
}
