{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "apple-select",
  "title": "Apple Select",
  "author": "AlgorizeTech",
  "description": "An iOS-style text selection rendered from a fixed character range — translucent highlight with lollipop handles on either end. Use it in docs, tutorials and marketing copy to point at part of a sentence.",
  "dependencies": [],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/custom/blocks/AppleSelectBlock.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport React from \"react\";\n\nexport interface AppleSelectProps\n  extends Omit<React.HTMLAttributes<HTMLElement>, \"color\"> {\n  /** The full sentence or paragraph to render. */\n  text: string;\n  /**\n   * The element to render. `\"span\"` sets the selection inline, inside a\n   * paragraph of your own, so only a phrase of running copy needs to go\n   * through the component.\n   */\n  as?: \"p\" | \"span\";\n  /**\n   * Character range to highlight as `[start, end]` — `end` is exclusive, the\n   * same way `String.slice` works — or several such ranges for more than one\n   * phrase. Out-of-range values are clamped; overlapping ranges are merged.\n   */\n  selection: [number, number] | [number, number][];\n  /** Accent for the highlight and handles. Any CSS colour; defaults to the theme primary. */\n  color?: string;\n  /** Show the iOS-style lollipop handles on either end of the range. */\n  handles?: boolean;\n  /** Fade the highlight in and pop the handles on mount. */\n  animate?: boolean;\n}\n\n// Keyframes ship inline so the component stays self-contained — no plugin,\n// no globals.css entry. Names are prefixed to avoid clashing with app styles.\n// With reduced motion the selection is simply there.\nconst KEYFRAMES = `\n@keyframes apple-select-highlight { from { opacity: 0; } to { opacity: 1; } }\n@keyframes apple-select-handle {\n  0% { opacity: 0; transform: scale(0.4); }\n  70% { opacity: 1; transform: scale(1.15); }\n  100% { opacity: 1; transform: scale(1); }\n}\n@media (prefers-reduced-motion: no-preference) {\n  .apple-select-highlight { animation: apple-select-highlight 0.4s ease-out both; }\n  .apple-select-handle { animation: apple-select-handle 0.35s cubic-bezier(0.34,1.56,0.64,1) 0.25s both; }\n}`;\n\n// The handle is a zero-width inline anchor with the bar drawn absolutely, so it\n// takes no room in the line box and never becomes a line-break opportunity —\n// it always wraps together with the word it is attached to.\nconst Handle = ({\n  edge,\n  color,\n  animate,\n}: {\n  edge: \"start\" | \"end\";\n  color: string;\n  animate: boolean;\n}) => (\n  <span\n    aria-hidden\n    className=\"relative inline\"\n    style={{\n      // Reserve a sliver next to the text so the bar does not touch the glyphs.\n      paddingInline: \"2px\",\n    }}\n  >\n    <span\n      className={cn(\"absolute w-[2px]\", animate && \"apple-select-handle\")}\n      style={{\n        // Centred inside the 4px anchor without a translate, which the\n        // entrance animation's scale() transform would otherwise override.\n        left: \"1px\",\n        top: \"-0.05em\",\n        height: \"1.15em\",\n        backgroundColor: color,\n        transformOrigin: edge === \"start\" ? \"top center\" : \"bottom center\",\n      }}\n    >\n      {/* The lollipop: start handle wears its knob on top, end handle underneath. */}\n      <span\n        className=\"absolute left-1/2 -translate-x-1/2 rounded-full\"\n        style={{\n          width: \"0.5em\",\n          height: \"0.5em\",\n          backgroundColor: color,\n          [edge === \"start\" ? \"top\" : \"bottom\"]: \"-0.42em\",\n        }}\n      />\n    </span>\n  </span>\n);\n\nexport const AppleSelect = React.forwardRef<HTMLElement, AppleSelectProps>(\n  (\n    {\n      text,\n      as: Tag = \"p\",\n      selection,\n      color = \"var(--primary)\",\n      handles = true,\n      animate = false,\n      className,\n      style,\n      ...props\n    },\n    ref\n  ) => {\n    // One range or many: clamp each so a stale index never throws or\n    // highlights backwards, sort them, and merge any that overlap.\n    const raw: [number, number][] =\n      Array.isArray(selection[0]) ? (selection as [number, number][]) : [selection as [number, number]];\n    const ranges = raw\n      .map(([a, b]) => {\n        const start = Math.min(Math.max(0, a), text.length);\n        const end = Math.min(Math.max(start, b), text.length);\n        return [start, end] as [number, number];\n      })\n      .filter(([a, b]) => b > a)\n      .sort((x, y) => x[0] - y[0])\n      .reduce<[number, number][]>((acc, r) => {\n        const last = acc[acc.length - 1];\n        if (last && r[0] <= last[1]) last[1] = Math.max(last[1], r[1]);\n        else acc.push([r[0], r[1]]);\n        return acc;\n      }, []);\n\n    const parts: React.ReactNode[] = [];\n    let cursor = 0;\n    ranges.forEach(([start, end], i) => {\n      parts.push(text.slice(cursor, start));\n      parts.push(\n        <React.Fragment key={i}>\n          {handles && <Handle edge=\"start\" color={color} animate={animate} />}\n          <mark\n            className={cn(\"rounded-[0.15em] px-[0.05em]\", animate && \"apple-select-highlight\")}\n            style={{\n              // Translucent tint keeps the text colour intact — like a real\n              // selection rather than a solid highlighter.\n              backgroundColor: `color-mix(in oklab, ${color} 35%, transparent)`,\n              color: \"inherit\",\n              // Wrapped lines each get their own rounded corners.\n              WebkitBoxDecorationBreak: \"clone\",\n              boxDecorationBreak: \"clone\",\n            }}\n          >\n            {text.slice(start, end)}\n          </mark>\n          {handles && <Handle edge=\"end\" color={color} animate={animate} />}\n        </React.Fragment>,\n      );\n      cursor = end;\n    });\n    parts.push(text.slice(cursor));\n\n    return (\n      <Tag\n        // Either tag takes the HTMLElement ref; TypeScript reads the union as <p>'s.\n        ref={ref as React.Ref<HTMLParagraphElement & HTMLSpanElement>}\n        // Inline, the line height is the surrounding paragraph's.\n        className={cn(Tag === \"p\" && \"leading-relaxed\", className)}\n        style={style}\n        {...props}\n      >\n        {animate && <style>{KEYFRAMES}</style>}\n        {parts}\n      </Tag>\n    );\n  }\n);\n\nAppleSelect.displayName = \"AppleSelect\";\n",
      "type": "registry:component",
      "target": "components/custom/blocks/AppleSelectBlock.tsx"
    }
  ],
  "docs": "Named export: AppleSelect. `text` is the full string and `selection` is a `[start, end]` character range (end exclusive, like String.slice) or an array of them for several phrases. The range is fixed by the author, not draggable. `color` takes any CSS colour, `handles={false}` drops the lollipops, and `animate` fades the highlight in on mount. Renders a <p> by default; `as=\"span\"` sets it inline inside your own paragraph. Other HTML attributes are forwarded to it.",
  "categories": [
    "typography"
  ],
  "type": "registry:component"
}
